Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changeset/twelve-poets-type.md
Original file line number Diff line number Diff line change
@@ -0,0 +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.
54 changes: 54 additions & 0 deletions packages/preview-middleware-client/src/flp/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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?: {
Expand Down Expand Up @@ -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;
}
Comment thread
avasilev-sap marked this conversation as resolved.

/**
* 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
});
Comment thread
avasilev-sap marked this conversation as resolved.
};

/**
* 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);
Comment thread
avasilev-sap marked this conversation as resolved.
}

/**
* Apply additional configuration and initialize sandbox.
*
Expand Down Expand Up @@ -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 () {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,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.

Expand Down
91 changes: 90 additions & 1 deletion packages/preview-middleware-client/test/unit/flp/init.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -19,6 +18,7 @@ import {
resetAppState,
setI18nTitle
} from '../../../src/flp/init';
import * as infoCenterMessage from '../../../src/utils/info-center-message';

describe('flp/init', () => {
afterEach(() => {
Expand Down Expand Up @@ -555,4 +555,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();
});
Comment thread
avasilev-sap marked this conversation as resolved.

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
})
);
});
});
});
Loading