diff --git a/CHANGELOG.md b/CHANGELOG.md index 27d35f621ed5..419a042b711c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott -Work in this release was contributed by @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, and @mstrokin. Thank you for your contributions! +Work in this release was contributed by @nwalters512, @aloisklink, @arturovt, @benjick, @maximepvrt, @mstrokin, and @kunal-511. Thank you for your contributions! - **feat(solidstart)!: Default to `--import` setup and add `autoInjectServerSentry` ([#14862](https://github.com/getsentry/sentry-javascript/pull/14862))** diff --git a/docs/migration/v8-to-v9.md b/docs/migration/v8-to-v9.md index b49a3e8f161e..5ca6fe2a4962 100644 --- a/docs/migration/v8-to-v9.md +++ b/docs/migration/v8-to-v9.md @@ -126,6 +126,12 @@ Older Typescript versions _may_ still work, but we will not test them anymore an To customize which files are deleted after upload, define the `filesToDeleteAfterUpload` array with globs. +### `@sentry/react` + +The `componentStack` field in the `ErrorBoundary` component is now typed as `string` instead of `string | null | undefined` for the `onError` and `onReset` lifecycle methods. This more closely matches the actual behavior of React, which always returns a `string` whenever a component stack is available. + +In the `onUnmount` lifecycle method, the `componentStack` field is now typed as `string | null`. The `componentStack` is `null` when no error has been thrown at time of unmount. + ### Uncategorized (TODO) TODO diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index fa397cb3b9ef..9925ef1a8308 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -17,6 +17,11 @@ export type FallbackRender = (errorData: { resetError(): void; }) => React.ReactElement; +type OnUnmountType = { + (error: null, componentStack: null, eventId: null): void; + (error: unknown, componentStack: string, eventId: string): void; +}; + export type ErrorBoundaryProps = { children?: React.ReactNode | (() => React.ReactNode); /** If a Sentry report dialog should be rendered on error */ @@ -42,15 +47,23 @@ export type ErrorBoundaryProps = { */ handled?: boolean | undefined; /** Called when the error boundary encounters an error */ - onError?: ((error: unknown, componentStack: string | undefined, eventId: string) => void) | undefined; + onError?: ((error: unknown, componentStack: string, eventId: string) => void) | undefined; /** Called on componentDidMount() */ onMount?: (() => void) | undefined; - /** Called if resetError() is called from the fallback render props function */ - onReset?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | undefined; - /** Called on componentWillUnmount() */ - onUnmount?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | undefined; + /** + * Called when the error boundary resets due to a reset call from the + * fallback render props function. + */ + onReset?: ((error: unknown, componentStack: string, eventId: string) => void) | undefined; + /** + * Called on componentWillUnmount() with the error, componentStack, and eventId. + * + * If the error boundary never encountered an error, the error + * componentStack, and eventId will be null. + */ + onUnmount?: OnUnmountType | undefined; /** Called before the error is captured by Sentry, allows for you to add tags or context using the scope */ - beforeCapture?: ((scope: Scope, error: unknown, componentStack: string | undefined) => void) | undefined; + beforeCapture?: ((scope: Scope, error: unknown, componentStack: string) => void) | undefined; }; type ErrorBoundaryState = @@ -65,7 +78,7 @@ type ErrorBoundaryState = eventId: string; }; -const INITIAL_STATE = { +const INITIAL_STATE: ErrorBoundaryState = { componentStack: null, error: null, eventId: null, @@ -104,20 +117,17 @@ class ErrorBoundary extends React.Component { if (beforeCapture) { - beforeCapture(scope, error, passedInComponentStack); + beforeCapture(scope, error, componentStack); } const handled = this.props.handled != null ? this.props.handled : !!this.props.fallback; const eventId = captureReactException(error, errorInfo, { mechanism: { handled } }); if (onError) { - onError(error, passedInComponentStack, eventId); + onError(error, componentStack, eventId); } if (showDialog) { this._lastEventId = eventId; @@ -143,7 +153,15 @@ class ErrorBoundary extends React.Component this.resetErrorBoundary(), + eventId: state.eventId, + }) + : fallback; + + if (React.isValidElement(element)) { + return element; } - if (typeof children === 'function') { - return (children as () => React.ReactNode)(); + if (fallback) { + DEBUG_BUILD && logger.warn('fallback did not produce a valid ReactElement'); } - return children; + + // Fail gracefully if no fallback provided or is not valid + return null; } }