From fa4cd1d47f7f91f2c1ce41900d705dc7cba81c69 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Mon, 8 Sep 2025 10:21:58 +0200 Subject: [PATCH] frontend-defaults: new display for app startup errors Signed-off-by: Patrik Oldsberg --- packages/frontend-defaults/report.api.md | 12 ++ packages/frontend-defaults/src/createApp.tsx | 6 + packages/frontend-defaults/src/index.ts | 1 + .../src/maybeCreateErrorPage.tsx | 116 ++++++++++++++++++ 4 files changed, 135 insertions(+) create mode 100644 packages/frontend-defaults/src/maybeCreateErrorPage.tsx diff --git a/packages/frontend-defaults/report.api.md b/packages/frontend-defaults/report.api.md index 6fd2536251..75392d2dc1 100644 --- a/packages/frontend-defaults/report.api.md +++ b/packages/frontend-defaults/report.api.md @@ -3,6 +3,8 @@ > Do not edit this file. It is a report generated by [API Extractor](https://api-extractor.com/). ```ts +import { AppError } from '@backstage/frontend-app-api'; +import { AppErrorTypes } from '@backstage/frontend-app-api'; import { Config } from '@backstage/config'; import { ConfigApi } from '@backstage/frontend-plugin-api'; import { CreateAppRouteBinder } from '@backstage/frontend-app-api'; @@ -45,6 +47,16 @@ export function discoverAvailableFeatures(config: Config): { features: (FrontendFeature | FrontendFeatureLoader)[]; }; +// @public +export function maybeCreateErrorPage( + app: { + errors?: AppError[]; + }, + options?: { + warningCodes?: Array; + }, +): JSX_2.Element | undefined; + // @public (undocumented) export function resolveAsyncFeatures(options: { config: Config; diff --git a/packages/frontend-defaults/src/createApp.tsx b/packages/frontend-defaults/src/createApp.tsx index 2c8a80791f..4ff67f6d5a 100644 --- a/packages/frontend-defaults/src/createApp.tsx +++ b/packages/frontend-defaults/src/createApp.tsx @@ -36,6 +36,7 @@ import { import appPlugin from '@backstage/plugin-app'; import { discoverAvailableFeatures } from './discovery'; import { resolveAsyncFeatures } from './resolution'; +import { maybeCreateErrorPage } from './maybeCreateErrorPage'; /** * Options for {@link createApp}. @@ -136,6 +137,11 @@ export function createApp(options?: CreateAppOptions): { advanced: options?.advanced, }); + const errorPage = maybeCreateErrorPage(app); + if (errorPage) { + return { default: () => errorPage }; + } + const rootEl = app.tree.root.instance!.getData( coreExtensionData.reactElement, ); diff --git a/packages/frontend-defaults/src/index.ts b/packages/frontend-defaults/src/index.ts index 68d4c72568..37ec62e794 100644 --- a/packages/frontend-defaults/src/index.ts +++ b/packages/frontend-defaults/src/index.ts @@ -24,3 +24,4 @@ export { createApp, type CreateAppOptions } from './createApp'; export { createPublicSignInApp } from './createPublicSignInApp'; export { discoverAvailableFeatures } from './discovery'; export { resolveAsyncFeatures } from './resolution'; +export { maybeCreateErrorPage } from './maybeCreateErrorPage'; diff --git a/packages/frontend-defaults/src/maybeCreateErrorPage.tsx b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx new file mode 100644 index 0000000000..c4bce8cf73 --- /dev/null +++ b/packages/frontend-defaults/src/maybeCreateErrorPage.tsx @@ -0,0 +1,116 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { FrontendPluginInfo } from '@backstage/frontend-plugin-api'; +import { JSX, useEffect, useState } from 'react'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { AppError, AppErrorTypes } from '@backstage/frontend-app-api'; + +const DEFAULT_WARNING_CODES: Array = [ + 'EXTENSION_IGNORED', + 'INVALID_EXTENSION_CONFIG_KEY', + 'EXTENSION_INPUT_DATA_IGNORED', + 'EXTENSION_OUTPUT_IGNORED', +]; + +function AppErrorItem(props: { error: AppError }): JSX.Element { + const { context } = props.error; + + const extensionId = + 'extensionId' in context ? context.extensionId : context.node.spec.id; + const node = 'node' in context ? context.node : undefined; + const plugin = 'plugin' in context ? context.plugin : node?.spec.plugin; + const pluginId = plugin?.id ?? 'N/A'; + + const [info, setInfo] = useState(undefined); + useEffect(() => { + plugin?.info().then(setInfo, error => { + // eslint-disable-next-line no-console + console.error(`Failed to load info for plugin ${plugin.id}: ${error}`); + }); + }, [plugin]); + + return ( +
+ {props.error.code}: {props.error.message} +
+        
extensionId: {extensionId}
+ {pluginId &&
pluginId: {pluginId}
} + {info && ( +
+ package: {info.packageName}@{info.version} +
+ )} +
+
+ ); +} + +function AppErrorPage(props: { errors: AppError[] }): JSX.Element { + return ( +
+

App startup failed

+ {props.errors.map((error, index) => ( + + ))} +
+ ); +} + +/** + * If there are any unrecoverable errors in the app, this will return an error page in the form of a JSX element. + * + * If there are any recoverable errors, they will always be logged as warnings in the console. + * @public + */ +export function maybeCreateErrorPage( + app: { errors?: AppError[] }, + options?: { + warningCodes?: Array; + }, +): JSX.Element | undefined { + if (!app.errors) { + return undefined; + } + + const errors = new Array(); + const warnings = new Array(); + + const warningCodes = new Set(options?.warningCodes ?? DEFAULT_WARNING_CODES); + for (const error of app.errors) { + if (warningCodes.has(error.code)) { + warnings.push(error); + } else { + errors.push(error); + } + } + + if (warnings.length > 0) { + // eslint-disable-next-line no-console + console.warn('App startup encountered warnings:'); + for (const warning of warnings) { + // eslint-disable-next-line no-console + console.warn(`${warning.code}: ${warning.message}`); + } + } + + if (errors.length === 0) { + return undefined; + } + + return ; +}