Skip to content

feat: AppLayout error boundaries v2#4296

Open
georgylobko wants to merge 10 commits intomainfrom
feat/app-layout-error-boundaries-v2
Open

feat: AppLayout error boundaries v2#4296
georgylobko wants to merge 10 commits intomainfrom
feat/app-layout-error-boundaries-v2

Conversation

@georgylobko
Copy link
Member

@georgylobko georgylobko commented Feb 25, 2026

Description

Error boundaries for AppLayout areas - all areas that load as widgets now have error boundary coverage.

Related links, issue #, if available: n/a

How has this been tested?

Review checklist

The following items are to be evaluated by the author(s) and the reviewer(s).

Correctness

  • Changes include appropriate documentation updates.
  • Changes are backward-compatible if not indicated, see CONTRIBUTING.md.
  • Changes do not include unsupported browser features, see CONTRIBUTING.md.
  • Changes were manually tested for accessibility, see accessibility guidelines.

Security

Testing

  • Changes are covered with new/existing unit tests?
  • Changes are covered with new/existing integration tests?

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice.

@codecov
Copy link

codecov bot commented Feb 25, 2026

Codecov Report

❌ Patch coverage is 98.71795% with 3 lines in your changes missing coverage. Please review.
✅ Project coverage is 97.44%. Comparing base (52a4127) to head (ed35697).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
...visual-refresh-toolbar/drawer/global-ai-drawer.tsx 93.18% 3 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff           @@
##             main    #4296   +/-   ##
=======================================
  Coverage   97.43%   97.44%           
=======================================
  Files         897      897           
  Lines       26350    26374   +24     
  Branches     9517     9523    +6     
=======================================
+ Hits        25675    25700   +25     
- Misses        632      668   +36     
+ Partials       43        6   -37     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@georgylobko georgylobko force-pushed the feat/app-layout-error-boundaries-v2 branch from 01e3a2e to b83aa6d Compare February 26, 2026 10:53
) : (
<ErrorBoundaryImpl
renderFallback={renderFallback}
onError={() => null}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We don't need to do anything here as we expect such errors to be handled in global event listeners to avoid over-reporting them.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these global listeners? Which telemetry sink do they forward events to?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

discussed in DM

return (
<>
<AppLayoutBuiltInErrorBoundary
renderFallback={() => (
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't know how to ensure code coverage here - I expect this code to be executed as a last resort - when (if) something really unexpected happens

Comment on lines +81 to +90
const AppLayoutWrapper = hasParentErrorBoundary ? ErrorBoundary : 'div';
const appLayoutWrapperProps = hasParentErrorBoundary
? {
onError: (error: any) =>
console.log('The error gets caught by the parent error boundary wrapping app layout: ', error),
}
: {};

return (
<AppLayoutWrapper {...(appLayoutWrapperProps as any)}>
Copy link
Member

@just-boris just-boris Mar 4, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional comment

How to avoid that many any

const appLayout = <AppLayout ... />;

return hasParentErrorBoundary ? <ErrorBoundary>{appLayout}<ErrorBoundary> : appLayout

},

trigger: {
iconSvg: Symbol() as any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is happening here?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a drawer with a broken trigger to test how the error boundary handles it

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense in hindsight, but it needs an explaining comment that all of this is intentional. Good case for // @ts-expect-error by the way

and same for all intentionally invalid content

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good point. addressed

<circle stroke-width="2" stroke="currentColor" fill="none" cx="8" cy="8" r="7" />
<circle stroke-width="2" stroke="currentColor" fill="none" cx="8" cy="8" r="3" />
</svg>`,
} as any,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this needed?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removed

// Test page for an app layout nested inside another through an iframe.
// Not a use case that's encouraged.
'app-layout/multi-layout-global-drawer-child-layout',
'app-layout/with-error-boundaries',
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I do not see any nested app layouts, why is it skipped?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed. I initially tested it with nested app layouts

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, this is a good idea for a test. That one crashing app layout does not break another

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, brought back nested app layouts

describe.each([true, false])(
'entire AppLayout wrapped with error boundary component : %p',
(wrappedWithErrorBoundary: boolean) => {
const onError = jest.fn();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is going to reset this spy function between tests?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

appLayoutState.widgetizedState;
return (
<>
<AppLayoutBuiltInErrorBoundary>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not enough, because the code above is outside the boundary. if there will be any undefined value, the page will crash

Expected implementation is how you did AppLayoutStateProvider. Zero code outside the boundary

You can create a reusable utility for it

function withErrorBoundary(Component) {
    return function WithBoundaryComponent() {
        return <AppLayoutBuiltInErrorBoundary><Component /></AppLayoutBuiltInErrorBoundary>
    }
}

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

const context = useContext(ErrorBoundariesContext);
const thisSuppressed = context.suppressed === true || context.suppressed === RootSuppressed;
const nextSuppressed = suppressNested || thisSuppressed;
return !thisSuppressed ? (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is enabling this suppressing condition? Us or the consuming teams?

The in-widget boundaries should be only controlled by us and only emit metrics to our telemetry sink

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Who is enabling this suppressing condition?

Consumers. This does not suppress in-widget EBs, but forwards suppress conditions down to the components tree. Refactored this code to make it more readable

wrapper,
suppressNested = false,
children,
renderFallback = () => <></>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this argument ever used? I cannot find an example

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it is used here:

<AppLayoutBuiltInErrorBoundary renderFallback={() => <div />}>

The empty div there instead of a react fragment ensures correct positioning for elements in the header

) : (
<ErrorBoundaryImpl
renderFallback={renderFallback}
onError={() => null}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What are these global listeners? Which telemetry sink do they forward events to?

{...context}
wrapper={wrapper}
renderFallback={renderFallback}
className={styles['app-layout-part-fallback']}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this class goes anywhere? Didn't we discuss that this component renders no DOM? What happened to that discussion?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, it applies display: contents to the EB root. Without this, components that depend on specific element ordering would break. This ensures they render correctly without adding extra styling.

display: contents;

@just-boris
Copy link
Member

Still see 3 unresolved discussions

Also forgot to call it on the previous review, but we need at least one integration test here. Because this is the only way to test against React 18 in our build

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants