From abab3947b1dd121facdee0965f732c7d202f5ea5 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Nov 2025 15:39:54 -0500 Subject: [PATCH 1/7] task: Log global errors and unhandled rejections Improve observability of errors that occur outside of the React tree and its `ErrorBoundary`. --- src/utils/editor-environment.js | 98 ++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/src/utils/editor-environment.js b/src/utils/editor-environment.js index 4943e38c..2a464371 100644 --- a/src/utils/editor-environment.js +++ b/src/utils/editor-environment.js @@ -1,7 +1,12 @@ /** * Internal dependencies */ -import { awaitGBKitGlobal, editorLoaded, getGBKit } from './bridge'; +import { + awaitGBKitGlobal, + editorLoaded, + getGBKit, + logException, +} from './bridge'; import { loadEditorAssets } from './editor-loader'; import { initializeVideoPressAjaxBridge } from './videopress-bridge'; import EditorLoadError from '../components/editor-load-error'; @@ -38,6 +43,7 @@ export function setUpEditorEnvironment() { .then( initializeVideoPressAjaxBridge ) .then( loadPluginsIfEnabled ) .then( initializeEditor ) + .then( setupGlobalErrorHandlers ) .catch( handleError ); } @@ -94,3 +100,93 @@ function handleError( err ) { document.body.innerHTML = errorDetails; editorLoaded(); } + +/** + * Determines if an error originated from GutenbergKit code rather than + * third-party scripts. + * + * @param {string|undefined} filename - The filename from the error event + * @param {Error|undefined} errorObj - The error object with stack trace + * @return {boolean} True if the error appears to be from GutenbergKit code + */ +function isGutenbergKitError( filename, errorObj ) { + // Check the filename first + if ( filename ) { + // GutenbergKit errors should have /gutenberg/ in the path or be from + // the same origin + if ( + filename.includes( '/gutenberg/' ) || + filename.includes( window.location.origin ) + ) { + return true; + } + // If filename is from a different origin, it's likely third-party + if ( filename.startsWith( 'http' ) ) { + return false; + } + } + + // If no filename, check the error stack trace + if ( errorObj?.stack ) { + const stack = errorObj.stack; + // Look for GutenbergKit-related paths in the stack + if ( + stack.includes( '/gutenberg/' ) || + stack.includes( window.location.origin ) + ) { + return true; + } + } + + // If we can't determine the origin, report it to be safe + // Better to have some noise than miss legitimate errors + return true; +} + +/** + * Sets up global error handlers to catch and report unhandled errors + * and promise rejections. + */ +function setupGlobalErrorHandlers() { + // Catch unhandled errors + window.addEventListener( 'error', ( event ) => { + // Filter out errors from third-party scripts + if ( ! isGutenbergKitError( event.filename, event.error ) ) { + return; + } + + const errorObj = event.error || new Error( event.message ); + + logException( errorObj, { + context: { + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }, + tags: {}, + isHandled: false, + handledBy: 'window.error', + } ); + } ); + + // Catch unhandled promise rejections + window.addEventListener( 'unhandledrejection', ( event ) => { + // Convert rejection reason to Error if it isn't already + const errorObj = + event.reason instanceof Error + ? event.reason + : new Error( String( event.reason ) ); + + // Filter out errors from third-party scripts + if ( ! isGutenbergKitError( undefined, errorObj ) ) { + return; + } + + logException( errorObj, { + context: {}, + tags: {}, + isHandled: false, + handledBy: 'unhandledrejection', + } ); + } ); +} From b3be75bce0c86fb8749e3cf9282b2d4f61513402 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Nov 2025 15:43:50 -0500 Subject: [PATCH 2/7] refactor: Extract global error handler logic --- src/utils/editor-environment.js | 98 +---------------------------- src/utils/global-error-handler.js | 100 ++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 96 deletions(-) create mode 100644 src/utils/global-error-handler.js diff --git a/src/utils/editor-environment.js b/src/utils/editor-environment.js index 2a464371..91ef3e17 100644 --- a/src/utils/editor-environment.js +++ b/src/utils/editor-environment.js @@ -1,16 +1,12 @@ /** * Internal dependencies */ -import { - awaitGBKitGlobal, - editorLoaded, - getGBKit, - logException, -} from './bridge'; +import { awaitGBKitGlobal, editorLoaded, getGBKit } from './bridge'; import { loadEditorAssets } from './editor-loader'; import { initializeVideoPressAjaxBridge } from './videopress-bridge'; import EditorLoadError from '../components/editor-load-error'; import { error } from './logger'; +import { setupGlobalErrorHandlers } from './global-error-handler'; import './editor-styles'; /** @@ -100,93 +96,3 @@ function handleError( err ) { document.body.innerHTML = errorDetails; editorLoaded(); } - -/** - * Determines if an error originated from GutenbergKit code rather than - * third-party scripts. - * - * @param {string|undefined} filename - The filename from the error event - * @param {Error|undefined} errorObj - The error object with stack trace - * @return {boolean} True if the error appears to be from GutenbergKit code - */ -function isGutenbergKitError( filename, errorObj ) { - // Check the filename first - if ( filename ) { - // GutenbergKit errors should have /gutenberg/ in the path or be from - // the same origin - if ( - filename.includes( '/gutenberg/' ) || - filename.includes( window.location.origin ) - ) { - return true; - } - // If filename is from a different origin, it's likely third-party - if ( filename.startsWith( 'http' ) ) { - return false; - } - } - - // If no filename, check the error stack trace - if ( errorObj?.stack ) { - const stack = errorObj.stack; - // Look for GutenbergKit-related paths in the stack - if ( - stack.includes( '/gutenberg/' ) || - stack.includes( window.location.origin ) - ) { - return true; - } - } - - // If we can't determine the origin, report it to be safe - // Better to have some noise than miss legitimate errors - return true; -} - -/** - * Sets up global error handlers to catch and report unhandled errors - * and promise rejections. - */ -function setupGlobalErrorHandlers() { - // Catch unhandled errors - window.addEventListener( 'error', ( event ) => { - // Filter out errors from third-party scripts - if ( ! isGutenbergKitError( event.filename, event.error ) ) { - return; - } - - const errorObj = event.error || new Error( event.message ); - - logException( errorObj, { - context: { - filename: event.filename, - lineno: event.lineno, - colno: event.colno, - }, - tags: {}, - isHandled: false, - handledBy: 'window.error', - } ); - } ); - - // Catch unhandled promise rejections - window.addEventListener( 'unhandledrejection', ( event ) => { - // Convert rejection reason to Error if it isn't already - const errorObj = - event.reason instanceof Error - ? event.reason - : new Error( String( event.reason ) ); - - // Filter out errors from third-party scripts - if ( ! isGutenbergKitError( undefined, errorObj ) ) { - return; - } - - logException( errorObj, { - context: {}, - tags: {}, - isHandled: false, - handledBy: 'unhandledrejection', - } ); - } ); -} diff --git a/src/utils/global-error-handler.js b/src/utils/global-error-handler.js new file mode 100644 index 00000000..5a6795ed --- /dev/null +++ b/src/utils/global-error-handler.js @@ -0,0 +1,100 @@ +/** + * Internal dependencies + */ +import { logException } from './bridge'; + +/** + * Sets up global error handlers to catch and report unhandled errors + * and promise rejections. + */ +export function setupGlobalErrorHandlers() { + // Catch unhandled errors + window.addEventListener( 'error', ( event ) => { + // Filter out errors from third-party scripts + if ( ! isGutenbergKitError( event.filename, event.error ) ) { + return; + } + + const errorObj = event.error || new Error( event.message ); + + logException( errorObj, { + context: { + filename: event.filename, + lineno: event.lineno, + colno: event.colno, + }, + tags: {}, + isHandled: false, + handledBy: 'window.error', + } ); + + // Don't prevent default - let errors also appear in browser console + // for debugging purposes + } ); + + // Catch unhandled promise rejections + window.addEventListener( 'unhandledrejection', ( event ) => { + // Convert rejection reason to Error if it isn't already + const errorObj = + event.reason instanceof Error + ? event.reason + : new Error( String( event.reason ) ); + + // Filter out errors from third-party scripts + if ( ! isGutenbergKitError( undefined, errorObj ) ) { + return; + } + + logException( errorObj, { + context: {}, + tags: {}, + isHandled: false, + handledBy: 'unhandledrejection', + } ); + + // Don't prevent default - let rejections also appear in browser console + // for debugging purposes + } ); +} + +/** + * Determines if an error originated from GutenbergKit code rather than + * third-party scripts. + * + * @param {string|undefined} filename - The filename from the error event + * @param {Error|undefined} errorObj - The error object with stack trace + * @return {boolean} True if the error appears to be from GutenbergKit code + */ +function isGutenbergKitError( filename, errorObj ) { + // Check the filename first + if ( filename ) { + // GutenbergKit errors should have /gutenberg/ in the path or be from + // the same origin + if ( + filename.includes( '/gutenberg/' ) || + filename.includes( window.location.origin ) + ) { + return true; + } + // If filename is from a different origin, it's likely third-party + if ( filename.startsWith( 'http' ) ) { + return false; + } + } + + // If no filename, check the error stack trace + if ( errorObj?.stack ) { + const stack = errorObj.stack; + // Look for GutenbergKit-related paths in the stack + if ( + stack.includes( '/gutenberg/' ) || + stack.includes( window.location.origin ) + ) { + return true; + } + } + + // If we can't determine the origin, report it to be safe + // Better to have some noise than miss legitimate errors + return true; +} From 3ffb089c75452757b4dd625f34936433561c9ebe Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Nov 2025 15:45:25 -0500 Subject: [PATCH 3/7] refactor: Remove unnecessary comments --- src/utils/global-error-handler.js | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/utils/global-error-handler.js b/src/utils/global-error-handler.js index 5a6795ed..b1cfdd6e 100644 --- a/src/utils/global-error-handler.js +++ b/src/utils/global-error-handler.js @@ -27,9 +27,6 @@ export function setupGlobalErrorHandlers() { isHandled: false, handledBy: 'window.error', } ); - - // Don't prevent default - let errors also appear in browser console - // for debugging purposes } ); // Catch unhandled promise rejections @@ -51,9 +48,6 @@ export function setupGlobalErrorHandlers() { isHandled: false, handledBy: 'unhandledrejection', } ); - - // Don't prevent default - let rejections also appear in browser console - // for debugging purposes } ); } From d4cf4e7b05da33e91344e0281de9252da4d73111 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Nov 2025 16:44:44 -0500 Subject: [PATCH 4/7] refactor: Remover redundant and erroneous `/gutenberg/` check This path is not used by the project and had not impact. The `window.location.origin` is what achieved the correct outcome. --- src/utils/global-error-handler.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/utils/global-error-handler.js b/src/utils/global-error-handler.js index b1cfdd6e..6b45c5c7 100644 --- a/src/utils/global-error-handler.js +++ b/src/utils/global-error-handler.js @@ -62,12 +62,8 @@ export function setupGlobalErrorHandlers() { function isGutenbergKitError( filename, errorObj ) { // Check the filename first if ( filename ) { - // GutenbergKit errors should have /gutenberg/ in the path or be from - // the same origin - if ( - filename.includes( '/gutenberg/' ) || - filename.includes( window.location.origin ) - ) { + // GutenbergKit errors should be from the same origin + if ( filename.includes( window.location.origin ) ) { return true; } // If filename is from a different origin, it's likely third-party @@ -79,11 +75,8 @@ function isGutenbergKitError( filename, errorObj ) { // If no filename, check the error stack trace if ( errorObj?.stack ) { const stack = errorObj.stack; - // Look for GutenbergKit-related paths in the stack - if ( - stack.includes( '/gutenberg/' ) || - stack.includes( window.location.origin ) - ) { + // Look for same-origin paths in the stack trace + if ( stack.includes( window.location.origin ) ) { return true; } } From 30d21f3e7ef07f878b6b79aec39bec8c5be02733 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Nov 2025 17:30:36 -0500 Subject: [PATCH 5/7] refactor: Simplify external error check logic --- src/utils/global-error-handler.js | 51 +++++++++++++++---------------- 1 file changed, 25 insertions(+), 26 deletions(-) diff --git a/src/utils/global-error-handler.js b/src/utils/global-error-handler.js index 6b45c5c7..72d1b54a 100644 --- a/src/utils/global-error-handler.js +++ b/src/utils/global-error-handler.js @@ -10,8 +10,7 @@ import { logException } from './bridge'; export function setupGlobalErrorHandlers() { // Catch unhandled errors window.addEventListener( 'error', ( event ) => { - // Filter out errors from third-party scripts - if ( ! isGutenbergKitError( event.filename, event.error ) ) { + if ( isExternalError( event.filename, event.error ) ) { return; } @@ -37,8 +36,7 @@ export function setupGlobalErrorHandlers() { ? event.reason : new Error( String( event.reason ) ); - // Filter out errors from third-party scripts - if ( ! isGutenbergKitError( undefined, errorObj ) ) { + if ( isExternalError( undefined, errorObj ) ) { return; } @@ -52,36 +50,37 @@ export function setupGlobalErrorHandlers() { } /** - * Determines if an error originated from GutenbergKit code rather than - * third-party scripts. + * Determines if an error originated from an external third-party script. + * + * Detects external HTTP(S) URLs from different origins (third-party scripts). + * GutenbergKit errors (same-origin, file://, or unknown sources) return false. * * @param {string|undefined} filename - The filename from the error event * @param {Error|undefined} errorObj - The error object with stack trace - * @return {boolean} True if the error appears to be from GutenbergKit code + * @return {boolean} True if the error is from an external source (should be filtered) */ -function isGutenbergKitError( filename, errorObj ) { - // Check the filename first - if ( filename ) { - // GutenbergKit errors should be from the same origin - if ( filename.includes( window.location.origin ) ) { - return true; - } - // If filename is from a different origin, it's likely third-party - if ( filename.startsWith( 'http' ) ) { - return false; - } +function isExternalError( filename, errorObj ) { + // Detect external HTTP(S) URLs from different origins (third-party scripts) + if ( + filename?.startsWith( 'http' ) && + ! filename.includes( window.location.origin ) + ) { + return true; } - // If no filename, check the error stack trace + // Check stack trace for external URLs if ( errorObj?.stack ) { - const stack = errorObj.stack; - // Look for same-origin paths in the stack trace - if ( stack.includes( window.location.origin ) ) { - return true; + const stackLines = errorObj.stack.split( '\n' ); + for ( const line of stackLines ) { + // Check if any line in stack contains external HTTP URL + if ( + line.includes( 'http' ) && + ! line.includes( window.location.origin ) + ) { + return true; + } } } - // If we can't determine the origin, report it to be safe - // Better to have some noise than miss legitimate errors - return true; + return false; } From 5a4191730de23add6a0e37669a0a4d53dba88bba Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Nov 2025 18:58:27 -0500 Subject: [PATCH 6/7] refactor: Rename setUpGlobalErrorHandlers --- src/utils/editor-environment.js | 4 ++-- src/utils/global-error-handler.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/utils/editor-environment.js b/src/utils/editor-environment.js index 91ef3e17..d2b48dc4 100644 --- a/src/utils/editor-environment.js +++ b/src/utils/editor-environment.js @@ -6,7 +6,7 @@ import { loadEditorAssets } from './editor-loader'; import { initializeVideoPressAjaxBridge } from './videopress-bridge'; import EditorLoadError from '../components/editor-load-error'; import { error } from './logger'; -import { setupGlobalErrorHandlers } from './global-error-handler'; +import { setUpGlobalErrorHandlers } from './global-error-handler'; import './editor-styles'; /** @@ -39,7 +39,7 @@ export function setUpEditorEnvironment() { .then( initializeVideoPressAjaxBridge ) .then( loadPluginsIfEnabled ) .then( initializeEditor ) - .then( setupGlobalErrorHandlers ) + .then( setUpGlobalErrorHandlers ) .catch( handleError ); } diff --git a/src/utils/global-error-handler.js b/src/utils/global-error-handler.js index 72d1b54a..86d73741 100644 --- a/src/utils/global-error-handler.js +++ b/src/utils/global-error-handler.js @@ -7,7 +7,7 @@ import { logException } from './bridge'; * Sets up global error handlers to catch and report unhandled errors * and promise rejections. */ -export function setupGlobalErrorHandlers() { +export function setUpGlobalErrorHandlers() { // Catch unhandled errors window.addEventListener( 'error', ( event ) => { if ( isExternalError( event.filename, event.error ) ) { From a117409995ffb5831c057553e3ce5da973bf54f8 Mon Sep 17 00:00:00 2001 From: David Calhoun Date: Thu, 20 Nov 2025 18:59:27 -0500 Subject: [PATCH 7/7] refactor: Set up global error handlers earlier Avoid missing error occurring during editor initialization. --- src/utils/editor-environment.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/utils/editor-environment.js b/src/utils/editor-environment.js index d2b48dc4..f9f39477 100644 --- a/src/utils/editor-environment.js +++ b/src/utils/editor-environment.js @@ -16,6 +16,8 @@ import './editor-styles'; * @return {Promise} Promise that resolves when initialization is complete */ export function setUpEditorEnvironment() { + setUpGlobalErrorHandlers(); + // Detect platform and add class to body for platform-specific styling if ( typeof window !== 'undefined' && window.webkit ) { document.body.classList.add( 'is-ios' ); @@ -39,7 +41,6 @@ export function setUpEditorEnvironment() { .then( initializeVideoPressAjaxBridge ) .then( loadPluginsIfEnabled ) .then( initializeEditor ) - .then( setUpGlobalErrorHandlers ) .catch( handleError ); }