Skip to content

Feature: lazy load reload - #297

Open
santipalenque wants to merge 2 commits into
mainfrom
feature/lazy-load-reload
Open

Feature: lazy load reload#297
santipalenque wants to merge 2 commits into
mainfrom
feature/lazy-load-reload

Conversation

@santipalenque

@santipalenque santipalenque commented Jul 23, 2026

Copy link
Copy Markdown
Contributor

https://app.clickup.com/t/9014802374/86bb1gp6z

Summary by CodeRabbit

  • New Features

    • Added automatic recovery for stale or unavailable application chunks by reloading the page.
    • Added safeguards to prevent repeated reload loops and improved reporting when recovery fails.
    • Added support for handling chunk-loading errors across lazy-loaded features and global application errors.
  • Tests

    • Added comprehensive coverage for chunk detection, reload behavior, error handling, and recovery scenarios.
  • Chores

    • Updated the package to version 5.0.45-beta.0.

@coderabbitai

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a React/webpack chunk-load recovery utility with per-build reload protection, global error handling, Sentry filtering, Jest coverage, a Webpack entry, and a package version update.

Changes

Chunk Error Recovery

Layer / File(s) Summary
Chunk error classification
src/utils/lazy-with-reload.js, src/utils/__tests__/lazy-with-reload.test.js
Detects webpack chunk errors and HTML fallback SyntaxError values, including custom filename patterns, with matching tests.
Per-build reload state
src/utils/lazy-with-reload.js, src/utils/__tests__/lazy-with-reload.test.js
Tracks build fingerprints in sessionStorage, reloads once per build, confirms recovery, and reports repeated failures through Sentry or the console.
Runtime recovery integration
src/utils/lazy-with-reload.js, src/utils/__tests__/lazy-with-reload.test.js
Adds the React.lazy wrapper, global error listeners with cleanup, pending-reload confirmation, and Sentry event filtering.
Bundle and release wiring
webpack.common.js, package.json
Adds the utility as a Webpack entry and updates the package version to 5.0.45-beta.0.

Estimated code review effort: 3 (Moderate) | ~25 minutes

Sequence Diagram(s)

sequenceDiagram
  participant ReactLazy
  participant lazyWithReload
  participant sessionStorage
  participant Window
  participant Sentry
  ReactLazy->>lazyWithReload: import module
  lazyWithReload->>sessionStorage: check build reload state
  lazyWithReload->>Window: reload on matching chunk error
  Window->>lazyWithReload: repeated failure after reload
  lazyWithReload->>Sentry: report recovery failure
Loading
🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and refers to the main change: adding lazy-load reload behavior.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/lazy-load-reload

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 4

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/utils/lazy-with-reload.js`:
- Around line 40-55: Update isHtmlParsedAsScriptError to recognize Firefox’s
SyntaxError wording, “expected expression, got '<',” in addition to the existing
“Unexpected token '<'” check. Preserve the existing SyntaxError detection and
filename pattern guard, while allowing either characteristic message to trigger
the stale-chunk fallback.
- Around line 124-143: Update reloadOnChunkError to track, in memory, whether a
reload has already been triggered during the current page lifecycle or tick
before consulting persisted reload state. Have subsequent concurrent chunk
failures return without calling reportRecoveryFailed, while preserving the
existing persisted-state check for failures occurring after navigation and the
current write/sessionStorage/reload flow for the first failure.
- Around line 210-224: Update chunkErrorSentryBeforeSend to extract the relevant
filename from each Sentry exception’s stack frames and pass it with the
exception type and value to isChunkLoadError. Preserve the existing filtering
behavior for genuine chunk-load errors while ensuring the HTML-as-script
SyntaxError path requires a filename matching chunkFilenamePattern.
- Around line 61-82: Compute and cache the build fingerprint eagerly during
module evaluation, before any lazy chunks can load, instead of initializing
cachedBuildFingerprint inside getBuildFingerprint. Keep getBuildFingerprint as a
read-only accessor and preserve the existing document-scripts fingerprinting and
non-browser fallback behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: a5f6667e-34d6-40cb-a728-10d5b64ff9b6

📥 Commits

Reviewing files that changed from the base of the PR and between 24b90d8 and 400adfb.

📒 Files selected for processing (4)
  • package.json
  • src/utils/__tests__/lazy-with-reload.test.js
  • src/utils/lazy-with-reload.js
  • webpack.common.js

Comment thread src/utils/lazy-with-reload.js
Comment on lines +61 to +82
// Fingerprints the statically-loaded <script> tags present the first time
// this module runs on a given page load - i.e. before any lazy chunk has
// been requested. This identifies "which build is currently running"
// without requiring the consuming app to expose a dedicated build-version
// global. Computed once and cached: document.scripts grows over time as
// webpack's runtime appends a <script> tag for every lazy chunk that loads,
// so reading it lazily-but-uncached would make the fingerprint drift within
// a single build, not just across a real deploy.
let cachedBuildFingerprint;
const getBuildFingerprint = () => {
if (cachedBuildFingerprint === undefined) {
cachedBuildFingerprint =
typeof document !== "undefined"
? Array.from(document.scripts)
.map((s) => s.src)
.filter(Boolean)
.sort()
.join(",")
: "";
}
return cachedBuildFingerprint;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== locate file =="
git ls-files | rg '(^|/)lazy-with-reload\.js$|package\.json$|webpack|vite|entry' || true

echo "== file outline =="
ast-grep outline src/utils/lazy-with-reload.js --view compact || true

echo "== relevant source =="
cat -n src/utils/lazy-with-reload.js

echo "== search usages =="
rg -n "lazy-with-reload|reloadOnChunkError|entry:|main:|webpack|lazyWithReload|reload" -S . --glob '!node_modules' --glob '!dist' --glob '!build' | head -200

Repository: OpenStackweb/openstack-uicore-foundation

Length of output: 25127


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== webpack common entry/snippet =="
cat -n webpack.common.js | sed -n '1,80p'

echo "== tests around fingerprint/session and script mocks =="
cat -n src/utils/__tests__/lazy-with-reload.test.js | sed -n '1,220p'

echo "== read-only model of the fingerprint/current-state race =="
node - <<'JS'
function initialState(scripts) {
  return { cachedBuildFingerprint: undefined, scripts };
}
function getBuildFingerprint(state) {
  if (state.cachedBuildFingerprint === undefined) {
    state.cachedBuildFingerprint = Array.from(state.scripts)
      .map(s => s.src)
      .filter(Boolean)
      .sort()
      .join(",");
  }
  return state.cachedBuildFingerprint;
}
function reloadOnChunkErrorFirst(state) {
  const currentBuild = getBuildFingerprint(state);
  return { rebuildNow: false, savedBuild: currentBuild };
}
function simulatePostReload(state, additionalScriptsLoadedBeforeNextFailure) {
  state.scripts.push(...additionalScriptsLoadedBeforeNextFailure.map(src => ({ src })));
  state.cachedBuildFingerprint = undefined;
}
function reloadOnChunkErrorNext(state) {
  const currentBuild = getBuildFingerprint(state);
  const storedBuilds = [state.cachedBuildFingerprint];
  return { rebuildNow: true, savedBuild: currentBuild, storedBuilds };
}
const initial = initialState([{ src: "main.js" }]);
const first = reloadOnChunkErrorFirst(initial);
simulatePostReload(initial, [{ src: "lazy1.js" }]);
const second = reloadOnChunkErrorNext(initial);
console.log({
  firstFingerprint: first.savedBuild,
  reloadStateBuildStored: first.savedBuild,
  secondFingerprint: second.savedBuild,
  fingerprintsDiffer: first.savedBuild !== second.savedBuild,
});
JS

Repository: OpenStackweb/openstack-uicore-foundation

Length of output: 17308


Compute the build fingerprint at bootstrap instead of lazily on the first chunk error.

getBuildFingerprint() is first called inside reloadOnChunkError, after any lazy chunks that happened to load successfully in this session may have already been appended to document.scripts. Since the guard stores the fingerprint seen at that first failure, the post-reload fingerprint can include a different set of loaded chunks and bypass the once-per-build throttle.

♻️ Suggested fix: compute the fingerprint eagerly at module evaluation time
-let cachedBuildFingerprint;
-const getBuildFingerprint = () => {
-  if (cachedBuildFingerprint === undefined) {
-    cachedBuildFingerprint =
-      typeof document !== "undefined"
-        ? Array.from(document.scripts)
-            .map((s) => s.src)
-            .filter(Boolean)
-            .sort()
-            .join(",")
-        : "";
-  }
-  return cachedBuildFingerprint;
-};
+const cachedBuildFingerprint =
+  typeof document !== "undefined"
+    ? Array.from(document.scripts)
+        .map((s) => s.src)
+        .filter(Boolean)
+        .sort()
+        .join(",")
+    : "";
+const getBuildFingerprint = () => cachedBuildFingerprint;

This still relies on this module being evaluated before app-level lazy chunk imports are attempted, which needs to hold since it is bundled as its own webpack entry.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Fingerprints the statically-loaded <script> tags present the first time
// this module runs on a given page load - i.e. before any lazy chunk has
// been requested. This identifies "which build is currently running"
// without requiring the consuming app to expose a dedicated build-version
// global. Computed once and cached: document.scripts grows over time as
// webpack's runtime appends a <script> tag for every lazy chunk that loads,
// so reading it lazily-but-uncached would make the fingerprint drift within
// a single build, not just across a real deploy.
let cachedBuildFingerprint;
const getBuildFingerprint = () => {
if (cachedBuildFingerprint === undefined) {
cachedBuildFingerprint =
typeof document !== "undefined"
? Array.from(document.scripts)
.map((s) => s.src)
.filter(Boolean)
.sort()
.join(",")
: "";
}
return cachedBuildFingerprint;
};
// Fingerprints the statically-loaded <script> tags present the first time
// this module runs on a given page load - i.e. before any lazy chunk has
// been requested. This identifies "which build is currently running"
// without requiring the consuming app to expose a dedicated build-version
// global. Computed once and cached: document.scripts grows over time as
// webpack's runtime appends a <script> tag for every lazy chunk that loads,
// so reading it lazily-but-uncached would make the fingerprint drift within
// a single build, not just across a real deploy.
const cachedBuildFingerprint =
typeof document !== "undefined"
? Array.from(document.scripts)
.map((s) => s.src)
.filter(Boolean)
.sort()
.join(",")
: "";
const getBuildFingerprint = () => cachedBuildFingerprint;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/lazy-with-reload.js` around lines 61 - 82, Compute and cache the
build fingerprint eagerly during module evaluation, before any lazy chunks can
load, instead of initializing cachedBuildFingerprint inside getBuildFingerprint.
Keep getBuildFingerprint as a read-only accessor and preserve the existing
document-scripts fingerprinting and non-browser fallback behavior.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@santipalenque Confirmed after verification — this one is real and worth fixing. Two additions to the bot's analysis:

The header comment (lines 61-68) claims the fingerprint captures the scripts "present the first time this module runs", but the cache is populated on first call, and the only call site is inside reloadOnChunkError (line 127) — at first-failure time, after webpack's runtime has appended script tags for any lazy chunks that loaded successfully. So the stored fingerprint includes a variable chunk set.

Concrete impact when the reload doesn't fix things (stale cached index.html — exactly the scenario the throttle exists for): fingerprint at failure #1 = static scripts + chunks loaded pre-deploy; after reload, failure #2's fingerprint = static scripts only → mismatch → a second reload the throttle should have blocked, and the "persisted after auto-reload" report is delayed one full cycle. The loop is bounded (it stops as soon as a fingerprint repeats), so the typical damage is one extra reload plus delayed failure telemetry — but it defeats the module's core "once per build" guarantee.

The suggested eager-at-module-evaluation fix is correct, with the caveat CodeRabbit already noted: it relies on this module being evaluated at bootstrap, before any lazy import — which holds for consumers that call initChunkErrorRecovery at app start.

Comment on lines +124 to +143
export const reloadOnChunkError = (error, filename, chunkFilenamePattern) => {
if (!isChunkLoadError(error, filename, chunkFilenamePattern)) return false;

const currentBuild = getBuildFingerprint();
const state = readReloadState();

if (state && state.build === currentBuild) {
reportRecoveryFailed(error, filename);
return false;
}

writeReloadState({ build: currentBuild });
try {
window.sessionStorage.setItem(PENDING_CONFIRMATION_KEY, "1");
} catch {
// ignore - see readReloadState/writeReloadState
}
window.location.reload();
return true;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Concurrent chunk failures on first occurrence can trigger a false "persisted after auto-reload" report.

reloadOnChunkError writes RELOAD_STATE_KEY synchronously (Line 135) before the actual navigation from window.location.reload() takes effect. If two lazy chunks fail around the same tick (e.g., two separate route-level lazyWithReload imports both hitting the same stale build), the first call writes state and reloads; the second call reads back the state just written, sees state.build === currentBuild, and reports via reportRecoveryFailed as if the reload already happened and failed to fix things — even though no reload has completed yet. This produces a spurious Sentry alert on every multi-chunk-failure page load, independent of whether recovery actually works.

♻️ Suggested fix: guard with an in-memory "reload already triggered this tick" flag
+let reloadTriggeredThisPage = false;
+
 export const reloadOnChunkError = (error, filename, chunkFilenamePattern) => {
   if (!isChunkLoadError(error, filename, chunkFilenamePattern)) return false;
 
+  if (reloadTriggeredThisPage) return true; // reload already in flight for this page load
+
   const currentBuild = getBuildFingerprint();
   const state = readReloadState();
 
   if (state && state.build === currentBuild) {
     reportRecoveryFailed(error, filename);
     return false;
   }
 
+  reloadTriggeredThisPage = true;
   writeReloadState({ build: currentBuild });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const reloadOnChunkError = (error, filename, chunkFilenamePattern) => {
if (!isChunkLoadError(error, filename, chunkFilenamePattern)) return false;
const currentBuild = getBuildFingerprint();
const state = readReloadState();
if (state && state.build === currentBuild) {
reportRecoveryFailed(error, filename);
return false;
}
writeReloadState({ build: currentBuild });
try {
window.sessionStorage.setItem(PENDING_CONFIRMATION_KEY, "1");
} catch {
// ignore - see readReloadState/writeReloadState
}
window.location.reload();
return true;
};
let reloadTriggeredThisPage = false;
export const reloadOnChunkError = (error, filename, chunkFilenamePattern) => {
if (!isChunkLoadError(error, filename, chunkFilenamePattern)) return false;
if (reloadTriggeredThisPage) return true; // reload already in flight for this page load
const currentBuild = getBuildFingerprint();
const state = readReloadState();
if (state && state.build === currentBuild) {
reportRecoveryFailed(error, filename);
return false;
}
reloadTriggeredThisPage = true;
writeReloadState({ build: currentBuild });
try {
window.sessionStorage.setItem(PENDING_CONFIRMATION_KEY, "1");
} catch {
// ignore - see readReloadState/writeReloadState
}
window.location.reload();
return true;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/lazy-with-reload.js` around lines 124 - 143, Update
reloadOnChunkError to track, in memory, whether a reload has already been
triggered during the current page lifecycle or tick before consulting persisted
reload state. Have subsequent concurrent chunk failures return without calling
reportRecoveryFailed, while preserving the existing persisted-state check for
failures occurring after navigation and the current write/sessionStorage/reload
flow for the first failure.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@santipalenque Confirmed after verification — real, and likely the common case rather than an edge case. A route rendering several lazyWithReload components mounts them together, so a stale build produces two or more import() rejections in the same microtask drain. window.location.reload() doesn't halt JS: the first rejection writes RELOAD_STATE_KEY synchronously (line 135) and triggers the reload; the second rejection's handler still runs, reads the state just written (line 130), sees the same build, and files "Chunk load error persisted after auto-reload" — a false recovery-failure alert on a recovery that is actually in flight. It also returns false, so lazyWithReload rethrows into the error boundary for a visible flash before navigation.

Since this fires on most multi-chunk recoveries, the recovery-failed Sentry signal — the one thing this module emits for monitoring — becomes mostly noise. The suggested in-memory reloadTriggeredThisPage guard is the right shape; please also add a test covering two same-tick failures (currently untested).

Comment on lines +210 to +224
// Sentry beforeSend hook: drops chunk-load-error events from being reported,
// since a first-time occurrence is expected to self-heal via the reload
// above. A repeat failure (reload didn't help) is reported separately and
// explicitly via reportRecoveryFailed's Sentry.captureMessage call, which is
// a message-type event (no .exception), so it is unaffected by this filter.
export const chunkErrorSentryBeforeSend = (event) => {
const exceptionValues = event.exception?.values || [];
const matchesChunkError = exceptionValues.some((exceptionValue) =>
isChunkLoadError({
name: exceptionValue.type,
message: exceptionValue.value
})
);
return matchesChunkError ? null : event;
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Sentry filter drops the filename cross-check the design explicitly calls for, over-broadening the match.

The header comment (Lines 30-37) stresses cross-checking the filename against chunkFilenamePattern so the HTML-as-script SyntaxError match "can't false-match an unrelated SyntaxError elsewhere in the app." But here isChunkLoadError is called with only {name, message} — no filename — so isHtmlParsedAsScriptError's !filename || ... check always short-circuits to true, and any SyntaxError with the message "Unexpected token '<'" gets silently dropped from Sentry, regardless of source. That message is a very common, generic symptom (e.g. HTML returned by a misconfigured API/proxy, auth redirect, etc.), so this filter risks masking unrelated production issues from monitoring.

Sentry exception values include stack frames that typically carry a filename; consider extracting the top frame's filename for the cross-check:

 export const chunkErrorSentryBeforeSend = (event) => {
   const exceptionValues = event.exception?.values || [];
-  const matchesChunkError = exceptionValues.some((exceptionValue) =>
-    isChunkLoadError({
-      name: exceptionValue.type,
-      message: exceptionValue.value
-    })
-  );
+  const matchesChunkError = exceptionValues.some((exceptionValue) => {
+    const frames = exceptionValue.stacktrace?.frames || [];
+    const culpritFilename = frames[frames.length - 1]?.filename;
+    return isChunkLoadError(
+      { name: exceptionValue.type, message: exceptionValue.value },
+      culpritFilename
+    );
+  });
   return matchesChunkError ? null : event;
 };
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// Sentry beforeSend hook: drops chunk-load-error events from being reported,
// since a first-time occurrence is expected to self-heal via the reload
// above. A repeat failure (reload didn't help) is reported separately and
// explicitly via reportRecoveryFailed's Sentry.captureMessage call, which is
// a message-type event (no .exception), so it is unaffected by this filter.
export const chunkErrorSentryBeforeSend = (event) => {
const exceptionValues = event.exception?.values || [];
const matchesChunkError = exceptionValues.some((exceptionValue) =>
isChunkLoadError({
name: exceptionValue.type,
message: exceptionValue.value
})
);
return matchesChunkError ? null : event;
};
// Sentry beforeSend hook: drops chunk-load-error events from being reported,
// since a first-time occurrence is expected to self-heal via the reload
// above. A repeat failure (reload didn't help) is reported separately and
// explicitly via reportRecoveryFailed's Sentry.captureMessage call, which is
// a message-type event (no .exception), so it is unaffected by this filter.
export const chunkErrorSentryBeforeSend = (event) => {
const exceptionValues = event.exception?.values || [];
const matchesChunkError = exceptionValues.some((exceptionValue) => {
const frames = exceptionValue.stacktrace?.frames || [];
const culpritFilename = frames[frames.length - 1]?.filename;
return isChunkLoadError(
{ name: exceptionValue.type, message: exceptionValue.value },
culpritFilename
);
});
return matchesChunkError ? null : event;
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/utils/lazy-with-reload.js` around lines 210 - 224, Update
chunkErrorSentryBeforeSend to extract the relevant filename from each Sentry
exception’s stack frames and pass it with the exception type and value to
isChunkLoadError. Preserve the existing filtering behavior for genuine
chunk-load errors while ensuring the HTML-as-script SyntaxError path requires a
filename matching chunkFilenamePattern.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@santipalenque Confirmed after verification — real, and narrower than the full problem. The filename-less isChunkLoadError call here (line 218) means any SyntaxError whose message contains "Unexpected token '<'" is silently dropped from Sentry, not just stale-chunk errors. Verified concrete case (V8/Chromium): Response.json() on an HTML error page (proxy 502, auth redirect) rejects with SyntaxError: Unexpected token '<', "<html>..." is not valid JSON — a class of production failures this filter would hide from monitoring permanently.

CodeRabbit's suggested fix (extract the culprit filename from the exception's stack frames and pass it through) is the right shape. Note the same filename-less gap also exists in onUnhandledRejection (line 189), where it causes page reloads rather than just suppressed events — see the full analysis and combined fix on the line 54 thread.

@smarcet
smarcet requested review from smarcet and a lite review from Copilot August 11, 2026 16:47
if (!looksLikeSyntaxError || !/Unexpected token '<'/i.test(message)) {
return false;
}
return !filename || chunkFilenamePattern.test(filename);

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@santipalenque The filename-less branch of the SyntaxError heuristic turns unrelated JSON.parse failures into page reloads and hides them from Sentry.

return !filename || chunkFilenamePattern.test(filename) matches any SyntaxError whose message contains "Unexpected token '<'" when no filename is available — and the two call sites that pass no filename are exactly the ones where unrelated errors arrive. Verified concrete scenario (V8/Chromium): new Response('<html>502</html>').json() rejects with SyntaxError: Unexpected token '<', "<html>502</html>" is not valid JSON. If that rejection goes unhandled (a fetch of an API that returned an HTML 502/proxy/auth page), onUnhandledRejection (line 189) passes it here with filename === undefined, and the user's page does a full reload mid-session — once per build, with the next occurrence filing a false "Chunk load error persisted after auto-reload" report. The same filename-less call in chunkErrorSentryBeforeSend (line 218) permanently drops this whole class of errors from Sentry monitoring.

Tightening this loses no genuine webpack case: webpack's JSONP runtime sets error.name = "ChunkLoadError" on every chunk-load failure it detects through the import() promise, including the "script responded 200 with HTML but the chunk never registered" case (webpack/lib/web/JsonpChunkLoadingRuntimeModule.js, loadingEnded handler, errorType 'missing') — so the ChunkLoadError path already covers the stale-chunk failure this branch is trying to catch.

Suggested fix:

  • In onUnhandledRejection, match only ChunkLoadError (import() rejections are never raw SyntaxErrors).
  • In isHtmlParsedAsScriptError, require a filename that matches chunkFilenamePattern (drop the !filename || escape) — the genuine HTML-as-script error arrives via the global error event with event.filename set.
  • In chunkErrorSentryBeforeSend, extract the culprit filename from the exception's stack frames and pass it through (as the existing CodeRabbit thread on the beforeSend hook suggests).
  • Update the test "matches a SyntaxError for 'Unexpected token <' with no filename" (lazy-with-reload.test.js line 57), which currently pins the permissive behavior, and add a regression test asserting a JSON.parse-style SyntaxError does NOT trigger a reload via unhandledrejection.

Copilot AI 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.

Pull request overview

Adds a reusable chunk-load recovery utility to the UI core library so applications can automatically recover from stale/unavailable lazy-loaded webpack chunks by triggering a guarded page reload, with reporting for non-recoverable cases.

Changes:

  • Added lazyWithReload, initChunkErrorRecovery, and Sentry filtering helpers to detect webpack chunk-load failures and recover via a one-time-per-build reload.
  • Added Jest coverage for detection, reload-guard behavior, global error/unhandledrejection handling, and Sentry filtering behavior.
  • Exposed the new utility via the webpack build entrypoints and bumped the package version to a beta release.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated 3 comments.

File Description
webpack.common.js Exposes the new utils/lazy-with-reload entry so consumers can import it from the built package.
src/utils/lazy-with-reload.js Implements chunk-load error detection, guarded reload recovery, and Sentry beforeSend filtering helper.
src/utils/tests/lazy-with-reload.test.js Adds unit tests covering detection and recovery behavior.
package.json Updates package version to 5.0.45-beta.0.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +61 to +63
// Fingerprints the statically-loaded <script> tags present the first time
// this module runs on a given page load - i.e. before any lazy chunk has
// been requested. This identifies "which build is currently running"
Comment on lines +51 to +53
if (!looksLikeSyntaxError || !/Unexpected token '<'/i.test(message)) {
return false;
}
Comment on lines +41 to +50
beforeEach(() => {
window.sessionStorage.clear();
window.SENTRY_DSN = "https://test.example/dsn";
mockCaptureMessage.mockClear();
console.error = jest.fn(); // eslint-disable-line no-console
console.log = jest.fn(); // eslint-disable-line no-console
delete window.location;
window.location = { reload: jest.fn() };
setDocumentScripts([]);
});

@smarcet smarcet left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

@santipalenque please review

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.

3 participants