From 87b13aa80b30ba10814beb0f8653a16944142565 Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Thu, 22 May 2025 14:59:51 -0500 Subject: [PATCH 1/5] Use `beforeCapture` to set level in Sentry scope Only set level if there is a reason to (override exists) --- src/components/ErrorBoundary.spec.tsx | 35 ++++++++++++++++++++++++++- src/components/ErrorBoundary.tsx | 13 ++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/src/components/ErrorBoundary.spec.tsx b/src/components/ErrorBoundary.spec.tsx index ece6bef0e..7a87fe678 100644 --- a/src/components/ErrorBoundary.spec.tsx +++ b/src/components/ErrorBoundary.spec.tsx @@ -10,7 +10,7 @@ const { testkit, sentryTransport } = sentryTestkit(); const ErrorComponent = () => { throw new Error('Test Error') }; describe('ErrorBoundary', () => { - beforeEach(() => { + beforeAll(() => { Sentry.init({ dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0', transport: sentryTransport, @@ -72,6 +72,39 @@ describe('ErrorBoundary', () => { spy.mockRestore(); }); + it('sets level appropriately', () => { + const spy = jest.spyOn(console, 'error'); + spy.mockImplementation(() => undefined); + + // Clear previous reports + testkit.reset(); + + const SessionExpiredComponent = () => { + throw new SessionExpiredError(); + }; + + // Should create warning (reports[0]) + renderer.create( + + + + ); + + // Should create error (reports[1]) + renderer.create( + + + + ); + + const reports = testkit.reports(); + expect(reports).toHaveLength(2); + expect(reports[0].level).toBe('warning'); + expect(reports[1].level).toBe('error'); + + spy.mockRestore(); + }); + it('can override fallback components for specific errors', () => { const spy = jest.spyOn(console, 'error') spy.mockImplementation(() => undefined); diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 3aacf99ab..684366765 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -20,6 +20,11 @@ const defaultErrorFallbacks = { }; +const errorLevelByType: Record = { + 'ScoresSyncError': 'warning', + 'SessionExpiredError': 'warning' +} + export const ErrorBoundary = ({ children, renderFallback, @@ -78,6 +83,14 @@ export const ErrorBoundary = ({ }} {...props} onReset={() => setError(null)} + beforeCapture={(scope, error) => { + if (error) { + const type = getTypeFromError(error); + if (type in errorLevelByType) { + scope.setLevel(errorLevelByType[type]); + } + } + }} > {renderElement} From dd5de867df8f15a5abac0d638e51396f3cf02f6f Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Tue, 27 May 2025 17:29:15 -0500 Subject: [PATCH 2/5] Allow specifying error level with fallback --- src/components/ErrorBoundary.spec.tsx | 63 +++++++++++++++++++++++++++ src/components/ErrorBoundary.tsx | 51 ++++++++++++++++------ 2 files changed, 101 insertions(+), 13 deletions(-) diff --git a/src/components/ErrorBoundary.spec.tsx b/src/components/ErrorBoundary.spec.tsx index 7a87fe678..fab63165b 100644 --- a/src/components/ErrorBoundary.spec.tsx +++ b/src/components/ErrorBoundary.spec.tsx @@ -105,6 +105,69 @@ describe('ErrorBoundary', () => { spy.mockRestore(); }); + it('can override level in error fallbacks', () => { + const spy = jest.spyOn(console, 'error'); + spy.mockImplementation(() => undefined); + + // Clear previous reports + testkit.reset(); + + const SessionExpiredComponent = () => { + throw new SessionExpiredError(); + }; + + // Round 1: Override default 'warning' level with 'debug' + // Should create debug (reports[0]) + const tree = renderer.create( + session expired, + level: 'debug' + } + }} + > + + + ); + + expect(tree).toMatchInlineSnapshot(`"session expired"`); + + // Should create error (reports[1]) + renderer.create( + + + + ); + + const reports = testkit.reports(); + expect(reports).toHaveLength(2); + expect(reports[0].level).toBe('debug'); + expect(reports[1].level).toBe('error'); + + // Round 2: Ensure 'error' level is default + testkit.reset(); + + expect(renderer.create( + I'm an error + } + }} + > + + + )).toMatchInlineSnapshot(`"I'm an error"`); + + expect(testkit.reports()).toHaveLength(1); + expect(testkit.reports()[0].level).toBe('error'); + + spy.mockRestore(); + }); + it('can override fallback components for specific errors', () => { const spy = jest.spyOn(console, 'error') spy.mockImplementation(() => undefined); diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 684366765..93e223aed 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -11,20 +11,41 @@ const Error = ({ children, ...props }: React.PropsWithChildren) const defaultErrorFallbacks = { 'generic': , - 'SessionExpiredError': - Please refresh your browser and try again. If this doesn't solve the problem, visit our Support Center. - , + 'SessionExpiredError': { + element: ( + + Please refresh your browser and try again. If this doesn't solve the problem, visit our Support Center. + + ), + level: 'warning' + } as const, 'UnauthorizedError': You may not have the required permissions or may have been logged out. Try refreshing the page or restarting your browser. If the issue persists, visit our Support Center. }; -const errorLevelByType: Record = { - 'ScoresSyncError': 'warning', - 'SessionExpiredError': 'warning' +interface FallbackWithOptions { + element: JSX.Element + level?: Sentry.SeverityLevel } +export interface ErrorFallback { + [_: string]: JSX.Element | FallbackWithOptions +} + +const isFallbackWithOptions = (thing: unknown): thing is FallbackWithOptions => ( + typeof thing == 'object' && thing !== null && 'element' in thing +); + +const getFallbackElement = (entry: JSX.Element | FallbackWithOptions | undefined) => ( + isFallbackWithOptions(entry) ? entry.element : entry +); + +const getFallbackLevel = (entry: JSX.Element | FallbackWithOptions | undefined) => ( + isFallbackWithOptions(entry) ? entry.level : undefined +); + export const ErrorBoundary = ({ children, renderFallback, @@ -36,11 +57,12 @@ export const ErrorBoundary = ({ renderFallback?: boolean; sentryDsn?: string; sentryInit?: Sentry.BrowserOptions; - errorFallbacks?: { [_: string]: JSX.Element } + errorFallbacks?: ErrorFallback, }) => { const [error, setError] = React.useState(null); - const errorFallbacks: { [_: string]: JSX.Element } = { ...defaultErrorFallbacks, ...props.errorFallbacks }; - const typedFallback = error?.type ? errorFallbacks[error.type] : undefined; + const errorFallbacks: ErrorFallback = { ...defaultErrorFallbacks, ...props.errorFallbacks }; + const fallbackEntry = error?.type ? errorFallbacks[error.type] : undefined + const typedFallback = getFallbackElement(fallbackEntry); const initCalled = React.useRef(false); // Optionally re-render with the children so they can display inline errors with @@ -81,16 +103,19 @@ export const ErrorBoundary = ({ eventId }); }} - {...props} - onReset={() => setError(null)} beforeCapture={(scope, error) => { + // We need to set the level here, before `setError` is called in `onError` + // throw -> beforeCapture -> error captured -> onError -> setError -> etc. if (error) { const type = getTypeFromError(error); - if (type in errorLevelByType) { - scope.setLevel(errorLevelByType[type]); + const errorLevel = getFallbackLevel(errorFallbacks[type]); + if (errorLevel) { + scope.setLevel(errorLevel); } } }} + {...props} + onReset={() => setError(null)} > {renderElement} From 8c398e6e0903247a5302d7d18ca189266aa911b3 Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Wed, 28 May 2025 09:23:52 -0500 Subject: [PATCH 3/5] Separate error level from fallback display --- src/components/ErrorBoundary.spec.tsx | 23 +++++--------- src/components/ErrorBoundary.tsx | 45 +++++++-------------------- 2 files changed, 19 insertions(+), 49 deletions(-) diff --git a/src/components/ErrorBoundary.spec.tsx b/src/components/ErrorBoundary.spec.tsx index fab63165b..60857bb5d 100644 --- a/src/components/ErrorBoundary.spec.tsx +++ b/src/components/ErrorBoundary.spec.tsx @@ -118,21 +118,14 @@ describe('ErrorBoundary', () => { // Round 1: Override default 'warning' level with 'debug' // Should create debug (reports[0]) - const tree = renderer.create( + renderer.create( session expired, - level: 'debug' - } - }} + errorLevels={{ SessionExpiredError: 'debug' }} > ); - - expect(tree).toMatchInlineSnapshot(`"session expired"`); // Should create error (reports[1]) renderer.create( @@ -149,18 +142,16 @@ describe('ErrorBoundary', () => { // Round 2: Ensure 'error' level is default testkit.reset(); - expect(renderer.create( + const unsetLevel = (undefined as unknown) as Sentry.SeverityLevel + + renderer.create( I'm an error - } - }} + errorLevels={{ SessionExpiredError: unsetLevel }} > - )).toMatchInlineSnapshot(`"I'm an error"`); + ); expect(testkit.reports()).toHaveLength(1); expect(testkit.reports()[0].level).toBe('error'); diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 93e223aed..f2592fbef 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -11,40 +11,18 @@ const Error = ({ children, ...props }: React.PropsWithChildren) const defaultErrorFallbacks = { 'generic': , - 'SessionExpiredError': { - element: ( - - Please refresh your browser and try again. If this doesn't solve the problem, visit our Support Center. - - ), - level: 'warning' - } as const, + 'SessionExpiredError': + Please refresh your browser and try again. If this doesn't solve the problem, visit our Support Center. + , 'UnauthorizedError': You may not have the required permissions or may have been logged out. Try refreshing the page or restarting your browser. If the issue persists, visit our Support Center. }; -interface FallbackWithOptions { - element: JSX.Element - level?: Sentry.SeverityLevel -} - -export interface ErrorFallback { - [_: string]: JSX.Element | FallbackWithOptions -} - -const isFallbackWithOptions = (thing: unknown): thing is FallbackWithOptions => ( - typeof thing == 'object' && thing !== null && 'element' in thing -); - -const getFallbackElement = (entry: JSX.Element | FallbackWithOptions | undefined) => ( - isFallbackWithOptions(entry) ? entry.element : entry -); - -const getFallbackLevel = (entry: JSX.Element | FallbackWithOptions | undefined) => ( - isFallbackWithOptions(entry) ? entry.level : undefined -); +const defaultErrorLevels: { [_: string]: Sentry.SeverityLevel } = { + 'SessionExpiredError': 'warning' +}; export const ErrorBoundary = ({ children, @@ -57,12 +35,13 @@ export const ErrorBoundary = ({ renderFallback?: boolean; sentryDsn?: string; sentryInit?: Sentry.BrowserOptions; - errorFallbacks?: ErrorFallback, + errorFallbacks?: { [_: string]: JSX.Element } + errorLevels?: { [_: string]: Sentry.SeverityLevel } }) => { const [error, setError] = React.useState(null); - const errorFallbacks: ErrorFallback = { ...defaultErrorFallbacks, ...props.errorFallbacks }; - const fallbackEntry = error?.type ? errorFallbacks[error.type] : undefined - const typedFallback = getFallbackElement(fallbackEntry); + const errorFallbacks: { [_: string]: JSX.Element } = { ...defaultErrorFallbacks, ...props.errorFallbacks }; + const errorLevels = { ...defaultErrorLevels, ...props.errorLevels }; + const typedFallback = error?.type ? errorFallbacks[error.type] : undefined; const initCalled = React.useRef(false); // Optionally re-render with the children so they can display inline errors with @@ -108,7 +87,7 @@ export const ErrorBoundary = ({ // throw -> beforeCapture -> error captured -> onError -> setError -> etc. if (error) { const type = getTypeFromError(error); - const errorLevel = getFallbackLevel(errorFallbacks[type]); + const errorLevel = errorLevels[type]; if (errorLevel) { scope.setLevel(errorLevel); } From 77b1004ab421971d199927152fd0882ae19558a5 Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Wed, 28 May 2025 09:24:20 -0500 Subject: [PATCH 4/5] Reintroduce default error level for `ScoresSyncError` --- src/components/ErrorBoundary.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index f2592fbef..376806ddf 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -21,6 +21,7 @@ const defaultErrorFallbacks = { }; const defaultErrorLevels: { [_: string]: Sentry.SeverityLevel } = { + 'ScoresSyncError': 'warning', 'SessionExpiredError': 'warning' }; From 5a1af93fccb5aa6cf8c22d796b7cb3c14551973e Mon Sep 17 00:00:00 2001 From: Tyler Nullmeier Date: Tue, 3 Jun 2025 15:28:21 -0500 Subject: [PATCH 5/5] Remove default level for `ScoresSyncError` --- src/components/ErrorBoundary.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/ErrorBoundary.tsx b/src/components/ErrorBoundary.tsx index 376806ddf..8b4e5c428 100644 --- a/src/components/ErrorBoundary.tsx +++ b/src/components/ErrorBoundary.tsx @@ -21,7 +21,6 @@ const defaultErrorFallbacks = { }; const defaultErrorLevels: { [_: string]: Sentry.SeverityLevel } = { - 'ScoresSyncError': 'warning', 'SessionExpiredError': 'warning' }; @@ -85,7 +84,7 @@ export const ErrorBoundary = ({ }} beforeCapture={(scope, error) => { // We need to set the level here, before `setError` is called in `onError` - // throw -> beforeCapture -> error captured -> onError -> setError -> etc. + // throw -> beforeCapture -> onError -> error captured -> setError -> etc. if (error) { const type = getTypeFromError(error); const errorLevel = errorLevels[type];