From 08023449875b5de55d5f5b357f351600f074d856 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 30 Apr 2026 15:59:21 +0300 Subject: [PATCH 1/4] feat: feat: Display unhandled exceptions from the Controller extension inside the Info Center. Register handlers on the globalThis object for error or promise rejection events. Any error containing in its stack trace the pattern /changes/coding/path/to/ts/or/js file will be displayed. This is a reliable check because all controllers reside in the changes/coding folder inside the adp project. --- .../preview-middleware-client/src/flp/init.ts | 54 +++++++++++ .../src/messagebundle.properties | 1 + .../test/unit/flp/init.test.ts | 91 ++++++++++++++++++- 3 files changed, 145 insertions(+), 1 deletion(-) diff --git a/packages/preview-middleware-client/src/flp/init.ts b/packages/preview-middleware-client/src/flp/init.ts index 4cdc860a04b..4559858d449 100644 --- a/packages/preview-middleware-client/src/flp/init.ts +++ b/packages/preview-middleware-client/src/flp/init.ts @@ -43,6 +43,10 @@ const UI5_LIBS = [ 'sap.zen' ]; +const CONTROLLER_EXTENSION_PATH_REGEX = /\/changes\/coding\/.+\.(js|ts)/; + +type GlobalErrorEvent = ErrorEvent | PromiseRejectionEvent; + interface Manifest { ['sap.ui5']?: { dependencies?: { @@ -278,6 +282,55 @@ function addCardGenerationUserAction(componentInstance: Component, container: ty }); } +/** + * Extracts an Error object from a global error event. + * Handles both synchronous errors (ErrorEvent) and unhandled promise rejections (PromiseRejectionEvent). + * + * @param {GlobalErrorEvent} event - The global error or unhandled rejection event. + * @returns {Error | undefined} The extracted Error instance, or undefined if no Error could be extracted. + */ +function extractError(event: GlobalErrorEvent): Error | undefined { + if ('error' in event && event.error instanceof Error) { + return event.error; + } + + if ('reason' in event && event.reason instanceof Error) { + return event.reason; + } + + return undefined; +} + +/** + * Reports controller extension errors to the Info Center. + * Filters events by checking if the stack trace contains 'ControllerExtension', + * and sends matching errors as error-level messages to the Info Center panel. + * + * @param {GlobalErrorEvent} event - The global error or unhandled rejection event. + */ +const reportControllerExtensionErrorToInfoCenter: (event: GlobalErrorEvent) => void = (event) => { + const error = extractError(event); + const stackTrace = error?.stack ?? ''; + if (!CONTROLLER_EXTENSION_PATH_REGEX.test(stackTrace)) { + return; + } + void sendInfoCenterMessage({ + title: { key: 'CONTROLLER_EXTENSION_UNHANDLED_ERROR_TITLE' }, + description: error?.message ?? '', + type: MessageBarType.error, + details: stackTrace + }); +}; + +/** + * Registers global event listeners for uncaught errors and unhandled promise rejections + * to detect and report controller extension errors to the Info Center. + */ +function registerForControllerExtensionErrors(): void { + globalThis.addEventListener('error', reportControllerExtensionErrorToInfoCenter); + globalThis.addEventListener('unhandledrejection', reportControllerExtensionErrorToInfoCenter); +} + /** * Apply additional configuration and initialize sandbox. * @@ -309,6 +362,7 @@ export async function init({ const ui5VersionInfo = await getUi5Version(); // Register RTA if configured if (flex) { + registerForControllerExtensionErrors(); const flexSettings = JSON.parse(flex) as FlexSettings; scenario = flexSettings.scenario; container.attachRendererCreatedEvent(async function () { diff --git a/packages/preview-middleware-client/src/messagebundle.properties b/packages/preview-middleware-client/src/messagebundle.properties index ed9c7fdffc8..44fa8ede48c 100644 --- a/packages/preview-middleware-client/src/messagebundle.properties +++ b/packages/preview-middleware-client/src/messagebundle.properties @@ -81,6 +81,7 @@ FLP_UI_INVALID_UI5_VERSION_DESCRIPTION = Invalid version info FLP_UI_VERSION_RETRIEVAL_FAILURE_DESCRIPTION = Could not get the SAPUI5 version of the application. Using {0} as fallback. FLP_UI5_VERSION_WARNING_TITLE = SAPUI5 Version Warning FLP_UI5_VERSION_WARNING_DESCRIPTION = The current SAPUI5 version set for this adaptation project is {0}. The minimum version for SAPUI5 Adaptation Project and its SAPUI5 Adaptation Editor is {1}. Install version {1} or higher. +CONTROLLER_EXTENSION_UNHANDLED_ERROR_TITLE = Controller extension unhandled error TABLE_ROWS_NEEDED_TO_CREATE_CUSTOM_COLUMN=At least one table row is required to create a new custom column. Make sure the table data is loaded and try again. diff --git a/packages/preview-middleware-client/test/unit/flp/init.test.ts b/packages/preview-middleware-client/test/unit/flp/init.test.ts index 16ea22191ac..54a3845f57e 100644 --- a/packages/preview-middleware-client/test/unit/flp/init.test.ts +++ b/packages/preview-middleware-client/test/unit/flp/init.test.ts @@ -8,7 +8,6 @@ import NewsAndPagesContainer from 'sap/cux/home/NewsAndPagesContainer'; import { CommunicationService } from 'open/ux/preview/client/cpe/communication-service'; import type Component from 'sap/ui/core/Component'; import type { InitRtaScript, RTAPlugin } from 'sap/ui/rta/api/startAdaptation'; -import { Window } from 'types/global'; import * as apiHandler from '../../../src/adp/api-handler'; import MyHomeController from '../../../src/flp/homepage/controller/MyHome.controller'; import { @@ -19,6 +18,7 @@ import { resetAppState, setI18nTitle } from '../../../src/flp/init'; +import * as infoCenterMessage from '../../../src/utils/info-center-message'; describe('flp/init', () => { afterEach(() => { @@ -512,4 +512,93 @@ describe('flp/init', () => { }); }); }); + + describe('registerForControllerExtensionErrors', () => { + let sendInfoCenterMessageSpy: jest.SpyInstance; + + beforeEach(async () => { + sendInfoCenterMessageSpy = jest + .spyOn(infoCenterMessage, 'sendInfoCenterMessage') + .mockResolvedValue(undefined); + + const flexSettings = { layer: 'CUSTOMER_BASE' }; + VersionInfo.load.mockResolvedValue({ + name: 'SAPUI5 Distribution', + libraries: [{ name: 'sap.ui.core', version: '1.118.1' }] + }); + CommunicationService.sendAction = jest.fn(); + await init({ flex: JSON.stringify(flexSettings) }); + }); + + afterEach(() => { + sendInfoCenterMessageSpy.mockRestore(); + }); + + test('reports error event with controller extension path in stack trace', async () => { + const error = new Error('Something went wrong'); + error.stack = 'Error: Something went wrong\n at /changes/coding/MyExtension.js:10:5'; + const errorEvent = new ErrorEvent('error', { error }); + globalThis.dispatchEvent(errorEvent); + + expect(sendInfoCenterMessageSpy).toHaveBeenCalledWith({ + title: { key: 'CONTROLLER_EXTENSION_UNHANDLED_ERROR_TITLE' }, + description: 'Something went wrong', + type: MessageBarType.error, + details: error.stack + }); + }); + + test('reports unhandled rejection with controller extension .ts path in stack trace', async () => { + const error = new Error('Async failure'); + error.stack = 'Error: Async failure\n at /changes/coding/MyExtension.ts:20:3'; + + const rejectionEvent = new Event('unhandledrejection') as any; + rejectionEvent.reason = error; + globalThis.dispatchEvent(rejectionEvent); + + expect(sendInfoCenterMessageSpy).toHaveBeenCalledWith({ + title: { key: 'CONTROLLER_EXTENSION_UNHANDLED_ERROR_TITLE' }, + description: 'Async failure', + type: MessageBarType.error, + details: error.stack + }); + }); + + test('ignores error event without controller extension path in stack trace', async () => { + const error = new Error('Unrelated error'); + error.stack = 'Error: Unrelated error\n at /some/other/path.js:5:1'; + const errorEvent = new ErrorEvent('error', { error }); + globalThis.dispatchEvent(errorEvent); + + expect(sendInfoCenterMessageSpy).not.toHaveBeenCalled(); + }); + + test('ignores error event when error is not an Error instance', async () => { + const errorEvent = new ErrorEvent('error', { error: 'string error' } as any); + globalThis.dispatchEvent(errorEvent); + + expect(sendInfoCenterMessageSpy).not.toHaveBeenCalled(); + }); + + test('ignores error event when error is undefined', async () => { + const errorEvent = new ErrorEvent('error', {}); + globalThis.dispatchEvent(errorEvent); + + expect(sendInfoCenterMessageSpy).not.toHaveBeenCalled(); + }); + + test('reports error with empty message when error.message is empty', async () => { + const error = new Error(''); + error.stack = 'Error\n at /changes/coding/Controller.js:1:1'; + const errorEvent = new ErrorEvent('error', { error }); + globalThis.dispatchEvent(errorEvent); + + expect(sendInfoCenterMessageSpy).toHaveBeenCalledWith( + expect.objectContaining({ + description: '', + type: MessageBarType.error + }) + ); + }); + }); }); From bea9031230c6bd5e166c311992332a0dce7c9a70 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 30 Apr 2026 16:10:46 +0300 Subject: [PATCH 2/4] chore: Add cset. --- .changeset/twelve-poets-type.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/twelve-poets-type.md diff --git a/.changeset/twelve-poets-type.md b/.changeset/twelve-poets-type.md new file mode 100644 index 00000000000..b6667fcb416 --- /dev/null +++ b/.changeset/twelve-poets-type.md @@ -0,0 +1,5 @@ +--- +'@sap-ux-private/preview-middleware-client': patch +--- + +feat: Display unhandled exceptions from the Controller extension inside the Info Center. From efc7b7e54ad90730079111285bc5b9dceba6a1a6 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Thu, 7 May 2026 15:33:06 +0300 Subject: [PATCH 3/4] fix: Change strings. --- packages/preview-middleware-client/src/messagebundle.properties | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/preview-middleware-client/src/messagebundle.properties b/packages/preview-middleware-client/src/messagebundle.properties index 44fa8ede48c..8f8dc154336 100644 --- a/packages/preview-middleware-client/src/messagebundle.properties +++ b/packages/preview-middleware-client/src/messagebundle.properties @@ -81,7 +81,7 @@ FLP_UI_INVALID_UI5_VERSION_DESCRIPTION = Invalid version info FLP_UI_VERSION_RETRIEVAL_FAILURE_DESCRIPTION = Could not get the SAPUI5 version of the application. Using {0} as fallback. FLP_UI5_VERSION_WARNING_TITLE = SAPUI5 Version Warning FLP_UI5_VERSION_WARNING_DESCRIPTION = The current SAPUI5 version set for this adaptation project is {0}. The minimum version for SAPUI5 Adaptation Project and its SAPUI5 Adaptation Editor is {1}. Install version {1} or higher. -CONTROLLER_EXTENSION_UNHANDLED_ERROR_TITLE = Controller extension unhandled error +CONTROLLER_EXTENSION_UNHANDLED_ERROR_TITLE = Controller Extension Unhandled Error TABLE_ROWS_NEEDED_TO_CREATE_CUSTOM_COLUMN=At least one table row is required to create a new custom column. Make sure the table data is loaded and try again. From dbfc3d86145319450e844cdc5c9ec34a05d15fa6 Mon Sep 17 00:00:00 2001 From: Atanas Vasilev Date: Fri, 8 May 2026 11:17:46 +0300 Subject: [PATCH 4/4] chore: Bump preview-middleware version. --- .changeset/twelve-poets-type.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.changeset/twelve-poets-type.md b/.changeset/twelve-poets-type.md index b6667fcb416..b9f19f131c6 100644 --- a/.changeset/twelve-poets-type.md +++ b/.changeset/twelve-poets-type.md @@ -1,5 +1,6 @@ --- '@sap-ux-private/preview-middleware-client': patch +"@sap-ux/preview-middleware": patch --- feat: Display unhandled exceptions from the Controller extension inside the Info Center.