From 7fbbb6f0f66a597a797b127a095a64091b48f3a0 Mon Sep 17 00:00:00 2001 From: Yevheniia Sorokina Date: Mon, 12 Oct 2026 12:14:07 +0100 Subject: [PATCH] Report page crashes, recover from stale chunks, and survive page translation - Errors caught by the error boundaries never reach window.onerror, so exception autocapture missed them. Both fallbacks now report the crash to PostHog, waiting up to 15 s for it to load and respecting an opt-out. - A first page load that asks for chunks a deploy removed fails before hydration, as an unhandled rejection, and leaves the page inert. It now reloads, at most once per page per tab in 10 minutes; a failure the guard blocks is reported. After the app has started, failed chunks (link preloads on hover) are left to Docusaurus. - Browser translation moves text nodes React owns, and React's next removeChild or insertBefore throws. A guard tolerates the moved node and logs a warning (the workaround from facebook/react#11538). translate="no" on the copyable commands, the agent picker and both framework pickers. - capturePostHogEvent and the crash reporter share one helper for calling PostHog (callPostHog), so the availability, opt-out and error rules live in one place. Co-Authored-By: Claude Opus 5.5 (1M context) --- docusaurus.config.ts | 2 + src/clientModules/chunkLoadRecovery.ts | 33 +++++++ src/clientModules/translationGuard.ts | 40 +++++++++ .../HomePage/Frameworks/Frameworks.tsx | 4 +- .../HomePage/Frameworks/FrameworksMobile.tsx | 4 +- src/components/SkillsCallout/CommandBlock.tsx | 3 + .../SkillsCallout/ManualInstall.tsx | 2 + src/components/SkillsCallout/analytics.ts | 30 ++----- src/theme/Error/index.js | 10 +++ src/theme/ErrorPageContent/index.js | 9 ++ src/utils/crashReporting.ts | 87 +++++++++++++++++++ src/utils/posthog.ts | 37 ++++++++ 12 files changed, 235 insertions(+), 26 deletions(-) create mode 100644 src/clientModules/chunkLoadRecovery.ts create mode 100644 src/clientModules/translationGuard.ts create mode 100644 src/theme/Error/index.js create mode 100644 src/theme/ErrorPageContent/index.js create mode 100644 src/utils/crashReporting.ts create mode 100644 src/utils/posthog.ts diff --git a/docusaurus.config.ts b/docusaurus.config.ts index e3c379db9b..d6ed88022c 100644 --- a/docusaurus.config.ts +++ b/docusaurus.config.ts @@ -955,6 +955,8 @@ const config: Config = { }, clientModules: [ require.resolve('./src/clientModules/agentSkillsNavLink.ts'), + require.resolve('./src/clientModules/translationGuard.ts'), + require.resolve('./src/clientModules/chunkLoadRecovery.ts'), ], plugins: [ [ diff --git a/src/clientModules/chunkLoadRecovery.ts b/src/clientModules/chunkLoadRecovery.ts new file mode 100644 index 0000000000..5c6e9edbdd --- /dev/null +++ b/src/clientModules/chunkLoadRecovery.ts @@ -0,0 +1,33 @@ +import { + isChunkLoadError, + reloadOnceForChunkError, + reportCrash, +} from '@site/src/utils/crashReporting'; + +/** + * Recover a first page load that asks for chunks a deploy has removed. + * + * A cached or long-open HTML page from the previous build references chunk + * hashes that no longer exist. Docusaurus already reloads when that happens + * during in-app navigation, but on the first load the route's chunks are + * fetched before hydration, and the failure is an unhandled rejection: the + * page stays as inert server-rendered HTML. Reload instead (see + * reloadOnceForChunkError for the loop guard). When the guard declines, the + * page will not start, so report it. + * + * Only until the app has started: after that, a failed chunk is a link + * preload on hover, and reloading the page the reader is on would be wrong. + */ +let started = false; + +if (typeof window !== 'undefined') { + window.addEventListener('unhandledrejection', (event) => { + if (started || !isChunkLoadError(event.reason)) return; + if (!reloadOnceForChunkError(event.reason)) reportCrash(event.reason, 'first-load'); + }); +} + +// Runs on the first render after hydration too, not only on navigation. +export function onRouteDidUpdate(): void { + started = true; +} diff --git a/src/clientModules/translationGuard.ts b/src/clientModules/translationGuard.ts new file mode 100644 index 0000000000..531cf36a14 --- /dev/null +++ b/src/clientModules/translationGuard.ts @@ -0,0 +1,40 @@ +/** + * Keep React working when the browser translates the page. + * + * Chrome and Edge translation replace text nodes with their own + * wrappers. When React later removes or inserts next to a node it believes it + * owns, the node is no longer where React left it, and the DOM throws + * "Failed to execute 'removeChild' on 'Node'", which takes the whole page + * down. + * + * The guard makes the two operations tolerate a node the translator moved: + * removing a node that is no longer a child does nothing, and inserting before + * one appends instead. Neither path runs unless the DOM would have thrown, and + * both log a warning so a real bug stays visible. This is the workaround from + * facebook/react#11538. + */ +if (typeof window !== 'undefined' && typeof Node === 'function' && Node.prototype) { + const originalRemoveChild = Node.prototype.removeChild; + Node.prototype.removeChild = function removeChild(this: Node, child: T): T { + if (child.parentNode !== this) { + console.warn('Cannot remove a child from a different parent', child, this); + return child; + } + return originalRemoveChild.call(this, child) as T; + }; + + const originalInsertBefore = Node.prototype.insertBefore; + Node.prototype.insertBefore = function insertBefore( + this: Node, + newNode: T, + referenceNode: Node | null, + ): T { + if (referenceNode && referenceNode.parentNode !== this) { + console.warn('Cannot insert before a reference node from a different parent', referenceNode, this); + return originalInsertBefore.call(this, newNode, null) as T; + } + return originalInsertBefore.call(this, newNode, referenceNode) as T; + }; +} + +export {}; diff --git a/src/components/HomePage/Frameworks/Frameworks.tsx b/src/components/HomePage/Frameworks/Frameworks.tsx index d2f12fb73f..64b1056064 100644 --- a/src/components/HomePage/Frameworks/Frameworks.tsx +++ b/src/components/HomePage/Frameworks/Frameworks.tsx @@ -64,7 +64,9 @@ export default function Frameworks({ handleFrameworkClick }: FrameworksProps) {

Select Your Framework to View Supported Products and Features

-
+ {/* Framework names are product names, and the labels change as the + selection does: left to a page translator they can crash React. */} + {frameworkCards.map((item) => { return (
{() => ( -
+ // Framework names are product names, and the labels change as the + // selection does: left to a page translator they can crash React. +
setOpenSelector(!openSelector)} diff --git a/src/components/SkillsCallout/CommandBlock.tsx b/src/components/SkillsCallout/CommandBlock.tsx index 9ec3459818..71303f5973 100644 --- a/src/components/SkillsCallout/CommandBlock.tsx +++ b/src/components/SkillsCallout/CommandBlock.tsx @@ -87,6 +87,9 @@ const CommandBlock: React.FC = ({
{command}