Address review feedback for feature flag isolation

Deduplicate the plugin/module feature flag registration loops and
distinguish the error source (Plugin vs Module). Treat
FEATURE_FLAG_INVALID as a warning in frontend-defaults.

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Made-with: Cursor
This commit is contained in:
Patrik Oldsberg
2026-04-23 14:47:52 +02:00
parent b6ca666812
commit 482cc5900a
4 changed files with 23 additions and 21 deletions
@@ -0,0 +1,5 @@
---
'@backstage/frontend-defaults': patch
---
Invalid feature flag declarations are now treated as warnings rather than errors, letting the app load normally.
@@ -177,6 +177,8 @@ describe('registerFeatureFlagDeclarationsInHolder', () => {
code: 'FEATURE_FLAG_INVALID',
context: { pluginId: 'my-plugin', flagName: 'mod/invalid' },
});
expect(errors[0].message).toContain("Module for plugin 'my-plugin'");
expect(errors[0].message).toContain("'mod/invalid'");
});
it('should isolate non-validation errors thrown by registerFlag', () => {
@@ -183,28 +183,22 @@ function registerFeatureFlagDeclarations(
collector: ErrorCollector,
) {
for (const feature of features) {
let pluginId: string | undefined;
let flags: Array<{ name: string; description?: string }> | undefined;
let source: string | undefined;
if (OpaqueFrontendPlugin.isType(feature)) {
const pluginId = feature.id;
for (const flag of OpaqueFrontendPlugin.toInternal(feature)
.featureFlags) {
try {
featureFlagApi.registerFlag({
name: flag.name,
description: flag.description,
pluginId,
});
} catch (error) {
collector.report({
code: 'FEATURE_FLAG_INVALID',
message: `Plugin '${pluginId}' declared invalid feature flag '${flag.name}': ${error}`,
context: { pluginId, flagName: flag.name, error: error as Error },
});
}
}
pluginId = feature.id;
flags = OpaqueFrontendPlugin.toInternal(feature).featureFlags;
source = 'Plugin';
} else if (isInternalFrontendModule(feature)) {
pluginId = feature.pluginId;
flags = toInternalFrontendModule(feature).featureFlags;
source = 'Module for plugin';
}
if (isInternalFrontendModule(feature)) {
const pluginId = feature.pluginId;
for (const flag of toInternalFrontendModule(feature).featureFlags) {
if (pluginId && flags && source) {
for (const flag of flags) {
try {
featureFlagApi.registerFlag({
name: flag.name,
@@ -214,7 +208,7 @@ function registerFeatureFlagDeclarations(
} catch (error) {
collector.report({
code: 'FEATURE_FLAG_INVALID',
message: `Plugin '${pluginId}' declared invalid feature flag '${flag.name}': ${error}`,
message: `${source} '${pluginId}' declared invalid feature flag '${flag.name}': ${error}`,
context: { pluginId, flagName: flag.name, error: error as Error },
});
}
@@ -26,6 +26,7 @@ const DEFAULT_WARNING_CODES: Array<keyof AppErrorTypes> = [
'EXTENSION_OUTPUT_IGNORED',
'EXTENSION_BOOTSTRAP_PREDICATE_IGNORED',
'EXTENSION_BOOTSTRAP_API_UNAVAILABLE',
'FEATURE_FLAG_INVALID',
];
function AppErrorItem(props: { error: AppError }): JSX.Element {