From 4ae3c7fd07c4745aa8932ff87388b7f50ecce130 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 21 May 2024 11:36:35 -0400 Subject: [PATCH 01/11] feat(react): Add Sentry.captureReactException --- packages/react/README.md | 56 ++++++++++++++-- packages/react/src/error.ts | 74 ++++++++++++++++++++++ packages/react/src/errorboundary.tsx | 58 ++--------------- packages/react/src/index.ts | 1 + packages/react/test/error.test.ts | 14 ++++ packages/react/test/errorboundary.test.tsx | 15 +---- 6 files changed, 148 insertions(+), 70 deletions(-) create mode 100644 packages/react/src/error.ts create mode 100644 packages/react/test/error.test.ts diff --git a/packages/react/README.md b/packages/react/README.md index 49c09247c9ea..1315d9760142 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -20,7 +20,7 @@ To use this SDK, call `Sentry.init(options)` before you mount your React compone ```javascript import React from 'react'; -import ReactDOM from 'react-dom'; +import { createRoot } from 'react-dom/client'; import * as Sentry from '@sentry/react'; Sentry.init({ @@ -30,12 +30,60 @@ Sentry.init({ // ... -ReactDOM.render(, rootNode); +const container = document.getElementById(“app”); +const root = createRoot(container); +root.render(); -// Can also use with React Concurrent Mode -// ReactDOM.createRoot(rootNode).render(); +// also works with hydrateRoot +// const domNode = document.getElementById('root'); +// const root = hydrateRoot(domNode, reactNode); +// root.render(); ``` +### React 19 + +Starting with React 19, the `createRoot` and `hydrateRoot` methods expose error hooks that can be used to capture errors automatically. Use the `Sentry.captureReactException` method to capture errors in the error hooks you are interested in. + +```js +const container = document.getElementById(“app”); +const root = createRoot(container, { + // Callback called when an error is thrown and not caught by an Error Boundary. + onUncaughtError: (error, errorInfo) => { + Sentry.captureReactException(error, errorInfo); + + console.error( + 'Uncaught error', + error, + errorInfo.componentStack + ); + }, + // Callback called when React catches an error in an Error Boundary. + onCaughtError: (error, errorInfo) => { + Sentry.captureReactException(error, errorInfo); + + console.error( + 'Caught error', + error, + errorInfo.componentStack + ); + }, + // Callback called when React automatically recovers from errors. + onRecoverableError: (error, errorInfo) => { + Sentry.captureReactException(error, errorInfo); + + console.error( + 'Recoverable error', + error, + error.cause, + errorInfo.componentStack, + ); + } +}); +root.render(); +``` + +If you want more finely grained control over error handling, we recommend only adding the `onUncaughtError` and `onRecoverableError` hooks and using an `ErrorBoundary` component instead of the `onCaughtError` hook. + ### ErrorBoundary `@sentry/react` exports an ErrorBoundary component that will automatically send Javascript errors from inside a diff --git a/packages/react/src/error.ts b/packages/react/src/error.ts new file mode 100644 index 000000000000..347793400d58 --- /dev/null +++ b/packages/react/src/error.ts @@ -0,0 +1,74 @@ +import { captureException } from '@sentry/browser'; +import type { EventHint } from '@sentry/types'; +import { isError } from '@sentry/utils'; +import { version } from 'react'; +import type { ErrorInfo } from 'react'; + +/** + * See if React major version is 17+ by parsing version string. + */ +export function isAtLeastReact17(reactVersion: string): boolean { + const reactMajor = reactVersion.match(/^([^.]+)/); + return reactMajor !== null && parseInt(reactMajor[0]) >= 17; +} + +/** + * Recurse through `error.cause` chain to set cause on an error. + */ +export function setCause(error: Error & { cause?: Error }, cause: Error): void { + const seenErrors = new WeakMap(); + + function recurse(error: Error & { cause?: Error }, cause: Error): void { + // If we've already seen the error, there is a recursive loop somewhere in the error's + // cause chain. Let's just bail out then to prevent a stack overflow. + if (seenErrors.has(error)) { + return; + } + if (error.cause) { + seenErrors.set(error, true); + return recurse(error.cause, cause); + } + error.cause = cause; + } + + recurse(error, cause); +} + +/** + * Captures an error that was thrown by a React ErrorBoundary or React root. + * + * @param error The error to capture. + * @param errorInfo The errorInfo provided by React. + * @param hint Optional additional data to attach to the Sentry event. + * @returns the id of the captured Sentry event. + */ +export function captureReactException( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + error: any, + { componentStack }: ErrorInfo, + hint?: EventHint, +): string { + // If on React version >= 17, create stack trace from componentStack param and links + // to to the original error using `error.cause` otherwise relies on error param for stacktrace. + // Linking errors requires the `LinkedErrors` integration be enabled. + // See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#native-component-stacks + // + // Although `componentDidCatch` is typed to accept an `Error` object, it can also be invoked + // with non-error objects. This is why we need to check if the error is an error-like object. + // See: https://github.com/getsentry/sentry-javascript/issues/6167 + if (isAtLeastReact17(version) && isError(error)) { + const errorBoundaryError = new Error(error.message); + errorBoundaryError.name = `React ErrorBoundary ${error.name}`; + errorBoundaryError.stack = componentStack; + + // Using the `LinkedErrors` integration to link the errors together. + setCause(error, errorBoundaryError); + } + + return captureException(error, { + ...hint, + captureContext: { + contexts: { react: { componentStack } }, + }, + }); +} diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 3ce1d0442b81..5cb706639b27 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -1,16 +1,12 @@ import type { ReportDialogOptions } from '@sentry/browser'; -import { captureException, getClient, showReportDialog, withScope } from '@sentry/browser'; +import { getClient, showReportDialog, withScope } from '@sentry/browser'; import type { Scope } from '@sentry/types'; -import { isError, logger } from '@sentry/utils'; +import { logger } from '@sentry/utils'; import hoistNonReactStatics from 'hoist-non-react-statics'; import * as React from 'react'; import { DEBUG_BUILD } from './debug-build'; - -export function isAtLeastReact17(version: string): boolean { - const major = version.match(/^([^.]+)/); - return major !== null && parseInt(major[0]) >= 17; -} +import { captureReactException } from './error'; export const UNKNOWN_COMPONENT = 'unknown'; @@ -69,25 +65,6 @@ const INITIAL_STATE = { eventId: null, }; -function setCause(error: Error & { cause?: Error }, cause: Error): void { - const seenErrors = new WeakMap(); - - function recurse(error: Error & { cause?: Error }, cause: Error): void { - // If we've already seen the error, there is a recursive loop somewhere in the error's - // cause chain. Let's just bail out then to prevent a stack overflow. - if (seenErrors.has(error)) { - return; - } - if (error.cause) { - seenErrors.set(error, true); - return recurse(error.cause, cause); - } - error.cause = cause; - } - - recurse(error, cause); -} - /** * A ErrorBoundary component that logs errors to Sentry. * NOTE: If you are a Sentry user, and you are seeing this stack frame, it means the @@ -118,38 +95,15 @@ class ErrorBoundary extends React.Component { - // If on React version >= 17, create stack trace from componentStack param and links - // to to the original error using `error.cause` otherwise relies on error param for stacktrace. - // Linking errors requires the `LinkedErrors` integration be enabled. - // See: https://reactjs.org/blog/2020/08/10/react-v17-rc.html#native-component-stacks - // - // Although `componentDidCatch` is typed to accept an `Error` object, it can also be invoked - // with non-error objects. This is why we need to check if the error is an error-like object. - // See: https://github.com/getsentry/sentry-javascript/issues/6167 - if (isAtLeastReact17(React.version) && isError(error)) { - const errorBoundaryError = new Error(error.message); - errorBoundaryError.name = `React ErrorBoundary ${error.name}`; - errorBoundaryError.stack = componentStack; - - // Using the `LinkedErrors` integration to link the errors together. - setCause(error, errorBoundaryError); - } - if (beforeCapture) { beforeCapture(scope, error, componentStack); } - const eventId = captureException(error, { - captureContext: { - contexts: { react: { componentStack } }, - }, - // If users provide a fallback component we can assume they are handling the error. - // Therefore, we set the mechanism depending on the presence of the fallback prop. - mechanism: { handled: !!this.props.fallback }, - }); + const eventId = captureReactException(error, errorInfo, { mechanism: { handled: !!this.props.fallback }}) if (onError) { onError(error, componentStack, eventId); diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index ef6627cf0c5e..b3f20deed03d 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,6 +1,7 @@ export * from '@sentry/browser'; export { init } from './sdk'; +export { captureReactException } from './error'; export { Profiler, withProfiler, useProfiler } from './profiler'; export type { ErrorBoundaryProps, FallbackRender } from './errorboundary'; export { ErrorBoundary, withErrorBoundary } from './errorboundary'; diff --git a/packages/react/test/error.test.ts b/packages/react/test/error.test.ts new file mode 100644 index 000000000000..780c6f9657fb --- /dev/null +++ b/packages/react/test/error.test.ts @@ -0,0 +1,14 @@ +import { isAtLeastReact17 } from '../src/error'; + +describe('isAtLeastReact17', () => { + test.each([ + ['React 16', '16.0.4', false], + ['React 17', '17.0.0', true], + ['React 17 with no patch', '17.4', true], + ['React 17 with no patch and no minor', '17', true], + ['React 18', '18.1.0', true], + ['React 19', '19.0.0', true], + ])('%s', (_: string, input: string, output: ReturnType) => { + expect(isAtLeastReact17(input)).toBe(output); + }); +}); diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 10c5130f88d7..d032fe73d6d3 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -5,7 +5,7 @@ import * as React from 'react'; import { useState } from 'react'; import type { ErrorBoundaryProps } from '../src/errorboundary'; -import { ErrorBoundary, UNKNOWN_COMPONENT, isAtLeastReact17, withErrorBoundary } from '../src/errorboundary'; +import { ErrorBoundary, UNKNOWN_COMPONENT, withErrorBoundary } from '../src/errorboundary'; const mockCaptureException = jest.fn(); const mockShowReportDialog = jest.fn(); @@ -581,16 +581,3 @@ describe('ErrorBoundary', () => { }); }); }); - -describe('isAtLeastReact17', () => { - test.each([ - ['React 16', '16.0.4', false], - ['React 17', '17.0.0', true], - ['React 17 with no patch', '17.4', true], - ['React 17 with no patch and no minor', '17', true], - ['React 18', '18.1.0', true], - ['React 19', '19.0.0', true], - ])('%s', (_: string, input: string, output: ReturnType) => { - expect(isAtLeastReact17(input)).toBe(output); - }); -}); From 4012bff11642b3e4c013966fd17a23b208a0e9a4 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Tue, 21 May 2024 18:39:05 -0400 Subject: [PATCH 02/11] yarn fix --- packages/react/src/errorboundary.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 5cb706639b27..9c175c45392a 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -103,7 +103,7 @@ class ErrorBoundary extends React.Component Date: Fri, 24 May 2024 15:32:28 -0400 Subject: [PATCH 03/11] use new API --- .github/workflows/build.yml | 1 + .../test-applications/react-19/.gitignore | 29 ++++++ .../test-applications/react-19/.npmrc | 2 + .../test-applications/react-19/package.json | 57 ++++++++++++ .../react-19/playwright.config.ts | 82 +++++++++++++++++ .../react-19/public/index.html | 24 +++++ .../react-19/src/globals.d.ts | 5 ++ .../test-applications/react-19/src/index.tsx | 26 ++++++ .../react-19/src/pages/Index.jsx | 51 +++++++++++ .../react-19/src/react-app-env.d.ts | 1 + .../react-19/start-event-proxy.mjs | 6 ++ .../react-19/tests/errors.test.ts | 34 +++++++ .../test-applications/react-19/tsconfig.json | 20 +++++ packages/react/README.md | 35 ++------ packages/react/package.json | 4 +- packages/react/src/error.ts | 32 +++++++ packages/react/src/index.ts | 2 +- yarn.lock | 88 +++++-------------- 18 files changed, 400 insertions(+), 99 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-19/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/react-19/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/react-19/package.json create mode 100644 dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/public/index.html create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx create mode 100644 dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-19/tsconfig.json diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index dd827c293a61..195e316581c3 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1009,6 +1009,7 @@ jobs: 'nextjs-app-dir', 'nextjs-14', 'nextjs-15', + 'react-19', 'react-create-hash-router', 'react-router-6-use-routes', 'react-router-5', diff --git a/dev-packages/e2e-tests/test-applications/react-19/.gitignore b/dev-packages/e2e-tests/test-applications/react-19/.gitignore new file mode 100644 index 000000000000..84634c973eeb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/.gitignore @@ -0,0 +1,29 @@ +# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. + +# dependencies +/node_modules +/.pnp +.pnp.js + +# testing +/coverage + +# production +/build + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/react-19/.npmrc b/dev-packages/e2e-tests/test-applications/react-19/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://127.0.0.1:4873 +@sentry-internal:registry=http://127.0.0.1:4873 diff --git a/dev-packages/e2e-tests/test-applications/react-19/package.json b/dev-packages/e2e-tests/test-applications/react-19/package.json new file mode 100644 index 000000000000..4c2f7d0df36e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/package.json @@ -0,0 +1,57 @@ +{ + "name": "react-19-test-app", + "version": "0.1.0", + "private": true, + "dependencies": { + "@sentry/react": "latest || *", + "@testing-library/jest-dom": "5.14.1", + "@testing-library/react": "13.0.0", + "@testing-library/user-event": "13.2.1", + "history": "4.9.0", + "@types/history": "4.7.11", + "@types/jest": "27.0.1", + "@types/node": "16.7.13", + "@types/react": "npm:types-react@rc", + "@types/react-dom": "npm:types-react-dom@rc", + "react": "19.0.0-rc-935180c7e0-20240524", + "react-dom": "19.0.0-rc-935180c7e0-20240524", + "react-scripts": "5.0.1", + "typescript": "4.9.5", + "web-vitals": "2.1.0" + }, + "scripts": { + "build": "react-scripts build", + "dev": "react-scripts start", + "start": "serve -s build", + "test": "playwright test", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && npx playwright install && pnpm build", + "test:assert": "pnpm test" + }, + "eslintConfig": { + "extends": [ + "react-app", + "react-app/jest" + ] + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all" + ], + "development": [ + "last 1 chrome version", + "last 1 firefox version", + "last 1 safari version" + ] + }, + "devDependencies": { + "@playwright/test": "^1.43.1", + "@sentry-internal/event-proxy-server": "link:../../../event-proxy-server", + "serve": "14.0.1" + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts b/dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts new file mode 100644 index 000000000000..3d7268ce5dc1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/playwright.config.ts @@ -0,0 +1,82 @@ +import type { PlaywrightTestConfig } from '@playwright/test'; +import { devices } from '@playwright/test'; + +const reactPort = 3030; +const eventProxyPort = 3031; + +/** + * See https://playwright.dev/docs/test-configuration. + */ +const config: PlaywrightTestConfig = { + testDir: './tests', + /* Maximum time one test can run for. */ + timeout: 150_000, + expect: { + /** + * Maximum time expect() should wait for the condition to be met. + * For example in `await expect(locator).toHaveText();` + */ + timeout: 5000, + }, + /* Run tests in files in parallel */ + fullyParallel: true, + /* Fail the build on CI if you accidentally left test.only in the source code. */ + forbidOnly: !!process.env.CI, + /* Retry on CI only */ + retries: 0, + /* Opt out of parallel tests on CI. */ + workers: 1, + /* Reporter to use. See https://playwright.dev/docs/test-reporters */ + reporter: 'list', + /* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */ + use: { + /* Maximum time each action such as `click()` can take. Defaults to 0 (no limit). */ + actionTimeout: 0, + + /* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */ + trace: 'on-first-retry', + + baseURL: `http://localhost:${reactPort}`, + }, + + /* Configure projects for major browsers */ + projects: [ + { + name: 'chromium', + use: { + ...devices['Desktop Chrome'], + }, + }, + // For now we only test Chrome! + // { + // name: 'firefox', + // use: { + // ...devices['Desktop Firefox'], + // }, + // }, + // { + // name: 'webkit', + // use: { + // ...devices['Desktop Safari'], + // }, + // }, + ], + + /* Run your local dev server before starting the tests */ + + webServer: [ + { + command: 'node start-event-proxy.mjs', + port: eventProxyPort, + }, + { + command: 'pnpm start', + port: reactPort, + env: { + PORT: `${reactPort}`, + }, + }, + ], +}; + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-19/public/index.html b/dev-packages/e2e-tests/test-applications/react-19/public/index.html new file mode 100644 index 000000000000..39da76522bea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/public/index.html @@ -0,0 +1,24 @@ + + + + + + + + React App + + + +
+ + + diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts b/dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts new file mode 100644 index 000000000000..ffa61ca49acc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/globals.d.ts @@ -0,0 +1,5 @@ +interface Window { + recordedTransactions?: string[]; + capturedExceptionId?: string; + sentryReplayId?: string; +} diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx new file mode 100644 index 000000000000..68bf719b5389 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx @@ -0,0 +1,26 @@ +import * as Sentry from '@sentry/react'; +// biome-ignore lint/nursery/noUnusedImports: +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import Index from './pages/Index'; + +Sentry.init({ + debug: true, + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: + process.env.REACT_APP_E2E_TEST_DSN || + 'https://3b6c388182fb435097f41d181be2b2ba@o4504321058471936.ingest.sentry.io/4504321066008576', + release: 'e2e-test', + tunnel: 'http://localhost:3031/', // proxy server +}); + +const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement, { + onUncaughtError: Sentry.reactErrorHandler(), + onCaughtError: Sentry.reactErrorHandler(), +}); + +root.render( +
+ +
+); diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx new file mode 100644 index 000000000000..eb1d29980066 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx @@ -0,0 +1,51 @@ +import * as React from 'react'; + +const Index = () => { + const [caughtError, setCaughtError] = React.useState(false); + const [uncaughtError, setUncaughtError] = React.useState(false); + + return ( + <> +
+ +

React 19

+ {caughtError && } + +
+
+
+ {uncaughtError && } + +
+ + ); +}; + +function Throw({error}) { + throw new Error(`${error} error`); +} + +class SampleErrorBoundary extends React.Component { + constructor(props) { + super(props); + this.state = {error: null}; + } + + componentDidCatch(error, errorInfo) { + this.setState({error}); + // no-op + } + + render() { + if (this.state.error) { + return
Caught an error: {this.state.error}
; + } + return this.props.children; + } +} + +export default Index; diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts b/dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts new file mode 100644 index 000000000000..6431bc5fc6b2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/src/react-app-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs new file mode 100644 index 000000000000..e0102436fdd0 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/event-proxy-server'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-19', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts new file mode 100644 index 000000000000..556fdf2d2477 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts @@ -0,0 +1,34 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/event-proxy-server'; + +test('Catches errors caught by error boundary', async ({ page }) => { + const errorEventPromise = waitForError('react-19', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=caughtError-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('caught error'); +}); + +test('Catches errors uncaught by error boundary', async ({ page }) => { + const errorEventPromise = waitForError('react-19', event => { + return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + }); + + await page.goto('/'); + + const exceptionButton = page.locator('id=uncaughtError-button'); + await exceptionButton.click(); + + const errorEvent = await errorEventPromise; + + expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values?.[0]?.value).toBe('uncaught error'); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-19/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-19/tsconfig.json new file mode 100644 index 000000000000..4cc95dc2689a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-19/tsconfig.json @@ -0,0 +1,20 @@ +{ + "compilerOptions": { + "target": "es2018", + "lib": ["dom", "dom.iterable", "esnext"], + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "noFallthroughCasesInSwitch": true, + "module": "esnext", + "moduleResolution": "node", + "resolveJsonModule": true, + "isolatedModules": true, + "noEmit": true, + "jsx": "react" + }, + "include": ["src", "tests"] +} diff --git a/packages/react/README.md b/packages/react/README.md index 1315d9760142..f77aefd187bd 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -42,42 +42,19 @@ root.render(); ### React 19 -Starting with React 19, the `createRoot` and `hydrateRoot` methods expose error hooks that can be used to capture errors automatically. Use the `Sentry.captureReactException` method to capture errors in the error hooks you are interested in. +Starting with React 19, the `createRoot` and `hydrateRoot` methods expose error hooks that can be used to capture errors automatically. Use the `Sentry.reactErrorHandler` function to capture errors in the error hooks you are interested in. ```js const container = document.getElementById(“app”); const root = createRoot(container, { // Callback called when an error is thrown and not caught by an Error Boundary. - onUncaughtError: (error, errorInfo) => { - Sentry.captureReactException(error, errorInfo); - - console.error( - 'Uncaught error', - error, - errorInfo.componentStack - ); - }, + onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + console.warn('Uncaught error', error, errorInfo.componentStack); + }), // Callback called when React catches an error in an Error Boundary. - onCaughtError: (error, errorInfo) => { - Sentry.captureReactException(error, errorInfo); - - console.error( - 'Caught error', - error, - errorInfo.componentStack - ); - }, + onCaughtError: Sentry.reactErrorHandler(), // Callback called when React automatically recovers from errors. - onRecoverableError: (error, errorInfo) => { - Sentry.captureReactException(error, errorInfo); - - console.error( - 'Recoverable error', - error, - error.cause, - errorInfo.componentStack, - ); - } + onRecoverableError: Sentry.reactErrorHandler(), }); root.render(); ``` diff --git a/packages/react/package.json b/packages/react/package.json index 9ab1ac01ec01..acf08902ebb1 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -67,8 +67,8 @@ "history-4": "npm:history@4.6.0", "history-5": "npm:history@4.9.0", "node-fetch": "^2.6.0", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "19@rc", + "react-dom": "19@rc", "react-router-3": "npm:react-router@3.2.0", "react-router-4": "npm:react-router@4.1.0", "react-router-5": "npm:react-router@5.0.0", diff --git a/packages/react/src/error.ts b/packages/react/src/error.ts index 347793400d58..cad2235c7c77 100644 --- a/packages/react/src/error.ts +++ b/packages/react/src/error.ts @@ -72,3 +72,35 @@ export function captureReactException( }, }); } + +/** + * Creates an error handler that can be used with the `onCaughtError`, `onUncaughtError`, + * and `onRecoverableError` options in `createRoot` and `hydrateRoot` React DOM methods. + * + * @param callback An optional callback that will be called after the error is captured. + * Use this to add custom handling for errors. + * + * @example + * + * ```JavaScript + * const root = createRoot(container, { + * onCaughtError: Sentry.reactErrorHandler(), + * onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + * console.warn('Caught error', error, errorInfo.componentStack); + * }); + * }); + * ``` + */ +export function reactErrorHandler( + // eslint-disable-next-line @typescript-eslint/no-explicit-any + callback?: (error: any, errorInfo: ErrorInfo, eventId: string) => void, + // eslint-disable-next-line @typescript-eslint/no-explicit-any +): (error: any, errorInfo: ErrorInfo) => void { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + return (error: any, errorInfo: ErrorInfo) => { + const eventId = captureReactException(error, errorInfo); + if (callback) { + callback(error, errorInfo, eventId); + } + }; +} diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts index b3f20deed03d..b0ee93d48677 100644 --- a/packages/react/src/index.ts +++ b/packages/react/src/index.ts @@ -1,7 +1,7 @@ export * from '@sentry/browser'; export { init } from './sdk'; -export { captureReactException } from './error'; +export { reactErrorHandler } from './error'; export { Profiler, withProfiler, useProfiler } from './profiler'; export type { ErrorBoundaryProps, FallbackRender } from './errorboundary'; export { ErrorBoundary, withErrorBoundary } from './errorboundary'; diff --git a/yarn.lock b/yarn.lock index 8819fb29597c..3f1d7e9fbd49 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8451,17 +8451,8 @@ dependencies: "@types/unist" "*" -"@types/history-4@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history-5@npm:@types/history@4.7.8": - version "4.7.8" - resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" - integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== - -"@types/history@*": +"@types/history-4@npm:@types/history@4.7.8", "@types/history-5@npm:@types/history@4.7.8", "@types/history@*": + name "@types/history-4" version "4.7.8" resolved "https://registry.yarnpkg.com/@types/history/-/history-4.7.8.tgz#49348387983075705fe8f4e02fb67f7daaec4934" integrity sha512-S78QIYirQcUoo6UJZx9CSP0O2ix9IaeAXwQi26Rhr/+mg7qqPy8TzaxHSUut7eGjL8WmLccT7/MXf304WjqHcA== @@ -8827,15 +8818,7 @@ "@types/history" "^3" "@types/react" "*" -"@types/react-router-4@npm:@types/react-router@5.1.14": - version "5.1.14" - resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" - integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== - dependencies: - "@types/history" "*" - "@types/react" "*" - -"@types/react-router-5@npm:@types/react-router@5.1.14": +"@types/react-router-4@npm:@types/react-router@5.1.14", "@types/react-router-5@npm:@types/react-router@5.1.14": version "5.1.14" resolved "https://registry.yarnpkg.com/@types/react-router/-/react-router-5.1.14.tgz#e0442f4eb4c446541ad7435d44a97f8fe6df40da" integrity sha512-LAJpqYUaCTMT2anZheoidiIymt8MuX286zoVFPM3DVb23aQBH0mAkFvzpd4LKqiolV8bBtZWT5Qp7hClCNDENw== @@ -26055,13 +26038,12 @@ rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@^18.0.0: - version "18.0.0" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.0.0.tgz#26b88534f8f1dbb80853e1eabe752f24100d8023" - integrity sha512-XqX7uzmFo0pUceWFCt7Gff6IyIMzFUn7QMZrbrQfGxtaxXZIcGQzoNpRLE3fQLnS4XzLLPMZX2T9TRcSrasicw== +react-dom@19@rc: + version "19.0.0-rc-f994737d14-20240522" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0-rc-f994737d14-20240522.tgz#992710eeed9ccc7546fd258745cff1e04a3f8659" + integrity sha512-J4CsfTSptPKkhaPbaR6n/KohQiHZTrRZ8GL4H8rbAqN/Qpy69g2MIoLBr5/PUX21ye6JxC1ZRWJFna7Xdg1pdA== dependencies: - loose-envify "^1.1.0" - scheduler "^0.21.0" + scheduler "0.25.0-rc-f994737d14-20240522" react-error-boundary@^3.1.0: version "3.1.1" @@ -26134,7 +26116,8 @@ react-is@^18.0.0: dependencies: "@remix-run/router" "1.0.2" -"react-router-6@npm:react-router@6.3.0": +"react-router-6@npm:react-router@6.3.0", react-router@6.3.0: + name react-router-6 version "6.3.0" resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== @@ -26149,12 +26132,10 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react-router@6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/react-router/-/react-router-6.3.0.tgz#3970cc64b4cb4eae0c1ea5203a80334fdd175557" - integrity sha512-7Wh1DzVQ+tlFjkeo+ujvjSqSJmkt1+8JO+T5xklPlgrh70y7ogx75ODRW0ThWhY7S+6yEDks8TYrtQe/aoboBQ== - dependencies: - history "^5.2.0" +react@19@rc: + version "19.0.0-rc-f994737d14-20240522" + resolved "https://registry.yarnpkg.com/react/-/react-19.0.0-rc-f994737d14-20240522.tgz#b400d554859940e9f4b8b6db1fd2537bd822bcc9" + integrity sha512-SeU2v5Xy6FotVhKz0pMS2gvYP7HlkF0qgTskj3JzA1vlxcb3dQjxlm9t0ZlJqcgoyI3VFAw7bomuDMdgy1nBuw== react@^18.0.0: version "18.0.0" @@ -27365,12 +27346,10 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@^0.21.0: - version "0.21.0" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.21.0.tgz#6fd2532ff5a6d877b6edb12f00d8ab7e8f308820" - integrity sha512-1r87x5fz9MXqswA2ERLo0EbOAU74DpIUO090gIasYTqlVoJeMcl+Z1Rg7WHz+qtPujhS/hGIt9kxZOYBV3faRQ== - dependencies: - loose-envify "^1.1.0" +scheduler@0.25.0-rc-f994737d14-20240522: + version "0.25.0-rc-f994737d14-20240522" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-f994737d14-20240522.tgz#6bbdbf50adeb250035a26f082bf966077d264a7e" + integrity sha512-qS+xGFF7AljP2APO2iJe8zESNsK20k25MACz+WGOXPybUsRdi1ssvaoF93im2nSX2q/XT3wKkjdz6RQfbmaxdw== schema-utils@^1.0.0: version "1.0.0" @@ -28474,7 +28453,7 @@ string-template@~0.2.1: resolved "https://registry.yarnpkg.com/string-template/-/string-template-0.2.1.tgz#42932e598a352d01fc22ec3367d9d84eec6c9add" integrity sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0= -"string-width-cjs@npm:string-width@^4.2.0": +"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -28500,15 +28479,6 @@ string-width@^2.1.0: is-fullwidth-code-point "^2.0.0" strip-ansi "^4.0.0" -string-width@^4.2.0, string-width@^4.2.2, string-width@^4.2.3: - version "4.2.3" - resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" - integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== - dependencies: - emoji-regex "^8.0.0" - is-fullwidth-code-point "^3.0.0" - strip-ansi "^6.0.1" - string-width@^5.0.1, string-width@^5.1.2: version "5.1.2" resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" @@ -28604,14 +28574,7 @@ stringify-object@^3.2.1: is-obj "^1.0.1" is-regexp "^1.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" - integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== - dependencies: - ansi-regex "^5.0.1" - -strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@6.0.1, strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -31238,7 +31201,7 @@ workerpool@^6.4.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-6.4.0.tgz#f8d5cfb45fde32fa3b7af72ad617c3369567a462" integrity sha512-i3KR1mQMNwY2wx20ozq2EjISGtQWDIfV56We+yGJ5yDs8jTwQiLLaqHlkBHITlCuJnYlVRmXegxFxZg7gqI++A== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== @@ -31256,15 +31219,6 @@ wrap-ansi@^6.0.1, wrap-ansi@^6.2.0: string-width "^4.1.0" strip-ansi "^6.0.0" -wrap-ansi@^7.0.0: - version "7.0.0" - resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" - integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== - dependencies: - ansi-styles "^4.0.0" - string-width "^4.1.0" - strip-ansi "^6.0.0" - wrap-ansi@^8.1.0: version "8.1.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" From 01ed8ad25505fce61b92180ff3cb15b0926113f8 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Fri, 24 May 2024 17:37:11 -0400 Subject: [PATCH 04/11] yarn fix and stuff --- .../test-applications/react-19/src/index.tsx | 2 +- .../react-19/src/pages/Index.jsx | 6 ++-- packages/react/README.md | 6 ++-- packages/react/package.json | 6 ++-- yarn.lock | 36 +++++++++++-------- 5 files changed, 32 insertions(+), 24 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx index 68bf719b5389..97a43f110a32 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx @@ -22,5 +22,5 @@ const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement, root.render(
-
+ , ); diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx index eb1d29980066..798e3ee8cddf 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx +++ b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx @@ -25,18 +25,18 @@ const Index = () => { ); }; -function Throw({error}) { +function Throw({ error }) { throw new Error(`${error} error`); } class SampleErrorBoundary extends React.Component { constructor(props) { super(props); - this.state = {error: null}; + this.state = { error: null }; } componentDidCatch(error, errorInfo) { - this.setState({error}); + this.setState({ error }); // no-op } diff --git a/packages/react/README.md b/packages/react/README.md index f77aefd187bd..5645b03d9fb0 100644 --- a/packages/react/README.md +++ b/packages/react/README.md @@ -42,7 +42,8 @@ root.render(); ### React 19 -Starting with React 19, the `createRoot` and `hydrateRoot` methods expose error hooks that can be used to capture errors automatically. Use the `Sentry.reactErrorHandler` function to capture errors in the error hooks you are interested in. +Starting with React 19, the `createRoot` and `hydrateRoot` methods expose error hooks that can be used to capture errors +automatically. Use the `Sentry.reactErrorHandler` function to capture errors in the error hooks you are interested in. ```js const container = document.getElementById(“app”); @@ -59,7 +60,8 @@ const root = createRoot(container, { root.render(); ``` -If you want more finely grained control over error handling, we recommend only adding the `onUncaughtError` and `onRecoverableError` hooks and using an `ErrorBoundary` component instead of the `onCaughtError` hook. +If you want more finely grained control over error handling, we recommend only adding the `onUncaughtError` and +`onRecoverableError` hooks and using an `ErrorBoundary` component instead of the `onCaughtError` hook. ### ErrorBoundary diff --git a/packages/react/package.json b/packages/react/package.json index acf08902ebb1..d2c473b2d8ad 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -58,7 +58,7 @@ "@types/history-5": "npm:@types/history@4.7.8", "@types/hoist-non-react-statics": "^3.3.1", "@types/node-fetch": "^2.6.0", - "@types/react": "^17.0.3", + "@types/react": "^18.0.0", "@types/react-router-3": "npm:@types/react-router@3.0.24", "@types/react-router-4": "npm:@types/react-router@5.1.14", "@types/react-router-5": "npm:@types/react-router@5.1.14", @@ -67,8 +67,8 @@ "history-4": "npm:history@4.6.0", "history-5": "npm:history@4.9.0", "node-fetch": "^2.6.0", - "react": "19@rc", - "react-dom": "19@rc", + "react": "^18.0.0", + "react-dom": "^18.0.0", "react-router-3": "npm:react-router@3.2.0", "react-router-4": "npm:react-router@4.1.0", "react-router-5": "npm:react-router@5.0.0", diff --git a/yarn.lock b/yarn.lock index 3f1d7e9fbd49..1ff8e6f200f9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8833,7 +8833,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0", "@types/react@^17.0.3": +"@types/react@*", "@types/react@>=16.9.0": version "17.0.3" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== @@ -8842,6 +8842,14 @@ "@types/scheduler" "*" csstype "^3.0.2" +"@types/react@^18.0.0": + version "18.3.3" + resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" + integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== + dependencies: + "@types/prop-types" "*" + csstype "^3.0.2" + "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6" @@ -26038,12 +26046,13 @@ rc@^1.2.7, rc@^1.2.8: minimist "^1.2.0" strip-json-comments "~2.0.1" -react-dom@19@rc: - version "19.0.0-rc-f994737d14-20240522" - resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-19.0.0-rc-f994737d14-20240522.tgz#992710eeed9ccc7546fd258745cff1e04a3f8659" - integrity sha512-J4CsfTSptPKkhaPbaR6n/KohQiHZTrRZ8GL4H8rbAqN/Qpy69g2MIoLBr5/PUX21ye6JxC1ZRWJFna7Xdg1pdA== +react-dom@^18.0.0: + version "18.3.1" + resolved "https://registry.yarnpkg.com/react-dom/-/react-dom-18.3.1.tgz#c2265d79511b57d479b3dd3fdfa51536494c5cb4" + integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== dependencies: - scheduler "0.25.0-rc-f994737d14-20240522" + loose-envify "^1.1.0" + scheduler "^0.23.2" react-error-boundary@^3.1.0: version "3.1.1" @@ -26132,11 +26141,6 @@ react-router-dom@^6.2.2: history "^5.2.0" react-router "6.3.0" -react@19@rc: - version "19.0.0-rc-f994737d14-20240522" - resolved "https://registry.yarnpkg.com/react/-/react-19.0.0-rc-f994737d14-20240522.tgz#b400d554859940e9f4b8b6db1fd2537bd822bcc9" - integrity sha512-SeU2v5Xy6FotVhKz0pMS2gvYP7HlkF0qgTskj3JzA1vlxcb3dQjxlm9t0ZlJqcgoyI3VFAw7bomuDMdgy1nBuw== - react@^18.0.0: version "18.0.0" resolved "https://registry.yarnpkg.com/react/-/react-18.0.0.tgz#b468736d1f4a5891f38585ba8e8fb29f91c3cb96" @@ -27346,10 +27350,12 @@ saxes@^6.0.0: dependencies: xmlchars "^2.2.0" -scheduler@0.25.0-rc-f994737d14-20240522: - version "0.25.0-rc-f994737d14-20240522" - resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.25.0-rc-f994737d14-20240522.tgz#6bbdbf50adeb250035a26f082bf966077d264a7e" - integrity sha512-qS+xGFF7AljP2APO2iJe8zESNsK20k25MACz+WGOXPybUsRdi1ssvaoF93im2nSX2q/XT3wKkjdz6RQfbmaxdw== +scheduler@^0.23.2: + version "0.23.2" + resolved "https://registry.yarnpkg.com/scheduler/-/scheduler-0.23.2.tgz#414ba64a3b282892e944cf2108ecc078d115cdc3" + integrity sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ== + dependencies: + loose-envify "^1.1.0" schema-utils@^1.0.0: version "1.0.0" From d854e19334e0ceba32f2f01a7ca19719bef7bfaa Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 10:54:17 -0400 Subject: [PATCH 05/11] fix e2e tests --- .../test-applications/react-19/src/index.tsx | 9 ++++++--- .../react-19/src/pages/Index.jsx | 2 +- .../react-19/tests/errors.test.ts | 16 ++++++++++++---- packages/react/package.json | 2 +- packages/react/src/errorboundary.tsx | 19 +++++++++++-------- yarn.lock | 8 ++++---- 6 files changed, 35 insertions(+), 21 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx index 97a43f110a32..6f6bb0640e73 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx +++ b/dev-packages/e2e-tests/test-applications/react-19/src/index.tsx @@ -5,7 +5,6 @@ import ReactDOM from 'react-dom/client'; import Index from './pages/Index'; Sentry.init({ - debug: true, environment: 'qa', // dynamic sampling bias to keep transactions dsn: process.env.REACT_APP_E2E_TEST_DSN || @@ -15,8 +14,12 @@ Sentry.init({ }); const root = ReactDOM.createRoot(document.getElementById('root') as HTMLElement, { - onUncaughtError: Sentry.reactErrorHandler(), - onCaughtError: Sentry.reactErrorHandler(), + onUncaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + console.warn(error, errorInfo); + }), + onCaughtError: Sentry.reactErrorHandler((error, errorInfo) => { + console.warn(error, errorInfo); + }), }); root.render( diff --git a/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx index 798e3ee8cddf..14fc3e9f97d1 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx +++ b/dev-packages/e2e-tests/test-applications/react-19/src/pages/Index.jsx @@ -42,7 +42,7 @@ class SampleErrorBoundary extends React.Component { render() { if (this.state.error) { - return
Caught an error: {this.state.error}
; + return
Caught an error: {JSON.stringify(this.state.error)}
; } return this.props.children; } diff --git a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts index 556fdf2d2477..9040d217d1bb 100644 --- a/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-19/tests/errors.test.ts @@ -2,8 +2,12 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/event-proxy-server'; test('Catches errors caught by error boundary', async ({ page }) => { + page.on('console', message => { + expect(message.text()).toContain('caught error'); + }); + const errorEventPromise = waitForError('react-19', event => { - return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + return !event.type && event.exception?.values?.[0]?.value === 'caught error'; }); await page.goto('/'); @@ -13,13 +17,17 @@ test('Catches errors caught by error boundary', async ({ page }) => { const errorEvent = await errorEventPromise; - expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values).toHaveLength(2); expect(errorEvent.exception?.values?.[0]?.value).toBe('caught error'); }); test('Catches errors uncaught by error boundary', async ({ page }) => { + page.on('console', message => { + expect(message.text()).toContain('uncaught error'); + }); + const errorEventPromise = waitForError('react-19', event => { - return !event.type && event.exception?.values?.[0]?.value === 'I am an error!'; + return !event.type && event.exception?.values?.[0]?.value === 'uncaught error'; }); await page.goto('/'); @@ -29,6 +37,6 @@ test('Catches errors uncaught by error boundary', async ({ page }) => { const errorEvent = await errorEventPromise; - expect(errorEvent.exception?.values).toHaveLength(1); + expect(errorEvent.exception?.values).toHaveLength(2); expect(errorEvent.exception?.values?.[0]?.value).toBe('uncaught error'); }); diff --git a/packages/react/package.json b/packages/react/package.json index d2c473b2d8ad..eca8754a4e91 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -56,7 +56,7 @@ "@testing-library/react-hooks": "^7.0.2", "@types/history-4": "npm:@types/history@4.7.8", "@types/history-5": "npm:@types/history@4.7.8", - "@types/hoist-non-react-statics": "^3.3.1", + "@types/hoist-non-react-statics": "^3.3.5", "@types/node-fetch": "^2.6.0", "@types/react": "^18.0.0", "@types/react-router-3": "npm:@types/react-router@3.0.24", diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 9c175c45392a..291c152a2948 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -36,13 +36,13 @@ export type ErrorBoundaryProps = { */ fallback?: React.ReactElement | FallbackRender | undefined; /** Called when the error boundary encounters an error */ - onError?: ((error: unknown, componentStack: string, eventId: string) => void) | undefined; + onError?: ((error: unknown, componentStack: string | undefined, 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, eventId: string | null) => void) | undefined; + onReset?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | undefined; /** Called on componentWillUnmount() */ - onUnmount?: ((error: unknown, componentStack: string | null, eventId: string | null) => void) | undefined; + onUnmount?: ((error: unknown, componentStack: string | null | undefined, eventId: string | null) => void) | 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; }; @@ -97,16 +97,19 @@ class ErrorBoundary extends React.Component { if (beforeCapture) { - beforeCapture(scope, error, componentStack); + beforeCapture(scope, error, passedInComponentStack); } const eventId = captureReactException(error, errorInfo, { mechanism: { handled: !!this.props.fallback } }); if (onError) { - onError(error, componentStack, eventId); + onError(error, passedInComponentStack, eventId); } if (showDialog) { this._lastEventId = eventId; @@ -186,7 +189,6 @@ function withErrorBoundary

>( WrappedComponent: React.ComponentType

, errorBoundaryOptions: ErrorBoundaryProps, ): React.FC

{ - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access const componentDisplayName = WrappedComponent.displayName || WrappedComponent.name || UNKNOWN_COMPONENT; const Wrapped: React.FC

= (props: P) => ( @@ -195,12 +197,13 @@ function withErrorBoundary

>( ); - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access Wrapped.displayName = `errorBoundary(${componentDisplayName})`; // Copy over static methods from Wrapped component to Profiler HOC // See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over - hoistNonReactStatics(Wrapped, WrappedComponent); + // Need to set type to any because of hoist-non-react-statics typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hoistNonReactStatics(Wrapped, WrappedComponent as any); return Wrapped; } diff --git a/yarn.lock b/yarn.lock index 1ff8e6f200f9..182bb280a80e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8462,10 +8462,10 @@ resolved "https://registry.yarnpkg.com/@types/history/-/history-3.2.4.tgz#0b6c62240d1fac020853aa5608758991d9f6ef3d" integrity sha512-q7x8QeCRk2T6DR2UznwYW//mpN5uNlyajkewH2xd1s1ozCS4oHFRg2WMusxwLFlE57EkUYsd/gCapLBYzV3ffg== -"@types/hoist-non-react-statics@^3.3.1": - version "3.3.1" - resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.1.tgz#1124aafe5118cb591977aeb1ceaaed1070eb039f" - integrity sha512-iMIqiko6ooLrTh1joXodJK5X9xeEALT1kM5G3ZLhD3hszxBdIEd5C75U834D9mLcINgD4OyZf5uQXjkuYydWvA== +"@types/hoist-non-react-statics@^3.3.5": + version "3.3.5" + resolved "https://registry.yarnpkg.com/@types/hoist-non-react-statics/-/hoist-non-react-statics-3.3.5.tgz#dab7867ef789d87e2b4b0003c9d65c49cc44a494" + integrity sha512-SbcrWzkKBw2cdwRTwQAswfpB9g9LJWfjtUeW/jvNwbhC8cpmmNYVePa+ncbUe0rGTQ7G3Ff6mYUN2VMfLVr+Sg== dependencies: "@types/react" "*" hoist-non-react-statics "^3.3.0" From 82ad70388d732445814e236cf0e54b04bb739f61 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 11:18:40 -0400 Subject: [PATCH 06/11] check componentStack --- packages/react/src/error.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/react/src/error.ts b/packages/react/src/error.ts index cad2235c7c77..1f5d04edbdf1 100644 --- a/packages/react/src/error.ts +++ b/packages/react/src/error.ts @@ -56,7 +56,7 @@ export function captureReactException( // Although `componentDidCatch` is typed to accept an `Error` object, it can also be invoked // with non-error objects. This is why we need to check if the error is an error-like object. // See: https://github.com/getsentry/sentry-javascript/issues/6167 - if (isAtLeastReact17(version) && isError(error)) { + if (isAtLeastReact17(version) && isError(error) && componentStack) { const errorBoundaryError = new Error(error.message); errorBoundaryError.name = `React ErrorBoundary ${error.name}`; errorBoundaryError.stack = componentStack; From 3cacb89b17df89edc55a91304b9569e45dbfb484 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 11:33:18 -0400 Subject: [PATCH 07/11] check profiler --- packages/react/src/profiler.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index 5008ffdc0010..dca938e38044 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -169,7 +169,9 @@ function withProfiler

>( // Copy over static methods from Wrapped component to Profiler HOC // See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over - hoistNonReactStatics(Wrapped, WrappedComponent); + // Need to set type to any because of hoist-non-react-statics typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hoistNonReactStatics(Wrapped, WrappedComponent as any); return Wrapped; } From 6ca6d1ad89329edcc22e05f7cb48dbcef4206cc7 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 11:47:44 -0400 Subject: [PATCH 08/11] the router too --- packages/react/src/reactrouter.tsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 3adf59326012..ce3c7579a3ba 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -244,7 +244,9 @@ export function withSentryRouting

, R extends React }; WrappedRoute.displayName = `sentryRoute(${componentDisplayName})`; - hoistNonReactStatics(WrappedRoute, Route); + // Need to set type to any because of hoist-non-react-statics typing + // eslint-disable-next-line @typescript-eslint/no-explicit-any + hoistNonReactStatics(WrappedRoute, Route as any); // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params: // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/13dc4235c069e25fe7ee16e11f529d909f9f3ff8/types/react-router/index.d.ts#L154-L164 From 1f1aaad5261f3a4fe166bc3c6b9f5fbd67fc8450 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 11:50:27 -0400 Subject: [PATCH 09/11] JSX type --- packages/react/src/reactrouter.tsx | 3 +-- packages/react/src/types.ts | 1 + packages/react/test/errorboundary.test.tsx | 14 +++++++------- packages/react/test/reactrouterv3.test.tsx | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index ce3c7579a3ba..5b5efbcec650 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -32,7 +32,7 @@ export type RouteConfig = { [propName: string]: unknown; path?: string | string[]; exact?: boolean; - component?: JSX.Element; + component?: React.JSX.Element; routes?: RouteConfig[]; }; @@ -245,7 +245,6 @@ export function withSentryRouting

, R extends React WrappedRoute.displayName = `sentryRoute(${componentDisplayName})`; // Need to set type to any because of hoist-non-react-statics typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any hoistNonReactStatics(WrappedRoute, Route as any); // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params: diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index ffb961bbfe3b..0374b9e47e80 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -1,5 +1,6 @@ // Disabling `no-explicit-any` for the whole file as `any` has became common requirement. /* eslint-disable @typescript-eslint/no-explicit-any */ +import type { JSX } from 'react'; export type Action = 'PUSH' | 'REPLACE' | 'POP'; diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index d032fe73d6d3..42a4c8b69be4 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -26,16 +26,16 @@ jest.mock('@sentry/browser', () => { }; }); -function Boo({ title }: { title: string }): JSX.Element { +function Boo({ title }: { title: string }): React.JSX.Element { throw new Error(title); } -function Bam(): JSX.Element { +function Bam(): React.JSX.Element { const [title] = useState('boom'); return ; } -function EffectSpyFallback({ error }: { error: unknown }): JSX.Element { +function EffectSpyFallback({ error }: { error: unknown }): React.JSX.Element { const [counter, setCounter] = useState(0); React.useEffect(() => { @@ -50,7 +50,7 @@ function EffectSpyFallback({ error }: { error: unknown }): JSX.Element { } interface TestAppProps extends ErrorBoundaryProps { - errorComp?: JSX.Element; + errorComp?: React.JSX.Element; } const TestApp: React.FC = ({ children, errorComp, ...props }) => { @@ -282,7 +282,7 @@ describe('ErrorBoundary', () => { it('does not set cause if non Error objected is thrown', () => { const TestAppThrowingString: React.FC = ({ children, ...props }) => { const [isError, setError] = React.useState(false); - function StringBam(): JSX.Element { + function StringBam(): React.JSX.Element { throw 'bam'; } return ( @@ -333,7 +333,7 @@ describe('ErrorBoundary', () => { it('handles when `error.cause` is nested', () => { const mockOnError = jest.fn(); - function CustomBam(): JSX.Element { + function CustomBam(): React.JSX.Element { const firstError = new Error('bam'); const secondError = new Error('bam2'); const thirdError = new Error('bam3'); @@ -378,7 +378,7 @@ describe('ErrorBoundary', () => { it('handles when `error.cause` is recursive', () => { const mockOnError = jest.fn(); - function CustomBam(): JSX.Element { + function CustomBam(): React.JSX.Element { const firstError = new Error('bam'); const secondError = new Error('bam2'); // @ts-expect-error Need to set cause on error diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index c207ead3aab3..d2c289a31abe 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -62,7 +62,7 @@ jest.mock('@sentry/core', () => { describe('browserTracingReactRouterV3', () => { const routes = ( -

{children}
}> +
{children}
}>
Home
} />
About
} />
Features
} /> From 4effd46100196150afdaef956ffe9d0b11e3f1f7 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 12:45:39 -0400 Subject: [PATCH 10/11] use weakset --- packages/react/src/error.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/react/src/error.ts b/packages/react/src/error.ts index 1f5d04edbdf1..ce83e39e33ee 100644 --- a/packages/react/src/error.ts +++ b/packages/react/src/error.ts @@ -16,7 +16,7 @@ export function isAtLeastReact17(reactVersion: string): boolean { * Recurse through `error.cause` chain to set cause on an error. */ export function setCause(error: Error & { cause?: Error }, cause: Error): void { - const seenErrors = new WeakMap(); + const seenErrors = new WeakSet(); function recurse(error: Error & { cause?: Error }, cause: Error): void { // If we've already seen the error, there is a recursive loop somewhere in the error's @@ -25,7 +25,7 @@ export function setCause(error: Error & { cause?: Error }, cause: Error): void { return; } if (error.cause) { - seenErrors.set(error, true); + seenErrors.add(error); return recurse(error.cause, cause); } error.cause = cause; From 9224b639d82a5d854a52dfc22457a6874f04ef36 Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 27 May 2024 13:53:05 -0400 Subject: [PATCH 11/11] types --- packages/react/package.json | 2 +- packages/react/src/errorboundary.tsx | 4 +--- packages/react/src/profiler.tsx | 4 +--- packages/react/src/reactrouter.tsx | 5 ++--- packages/react/src/types.ts | 1 - packages/react/test/errorboundary.test.tsx | 16 ++++++++-------- packages/react/test/reactrouterv3.test.tsx | 2 +- yarn.lock | 10 +--------- 8 files changed, 15 insertions(+), 29 deletions(-) diff --git a/packages/react/package.json b/packages/react/package.json index eca8754a4e91..fe934846d07b 100644 --- a/packages/react/package.json +++ b/packages/react/package.json @@ -58,7 +58,7 @@ "@types/history-5": "npm:@types/history@4.7.8", "@types/hoist-non-react-statics": "^3.3.5", "@types/node-fetch": "^2.6.0", - "@types/react": "^18.0.0", + "@types/react": "17.0.3", "@types/react-router-3": "npm:@types/react-router@3.0.24", "@types/react-router-4": "npm:@types/react-router@5.1.14", "@types/react-router-5": "npm:@types/react-router@5.1.14", diff --git a/packages/react/src/errorboundary.tsx b/packages/react/src/errorboundary.tsx index 291c152a2948..e12ca9f44d79 100644 --- a/packages/react/src/errorboundary.tsx +++ b/packages/react/src/errorboundary.tsx @@ -201,9 +201,7 @@ function withErrorBoundary

>( // Copy over static methods from Wrapped component to Profiler HOC // See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over - // Need to set type to any because of hoist-non-react-statics typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - hoistNonReactStatics(Wrapped, WrappedComponent as any); + hoistNonReactStatics(Wrapped, WrappedComponent); return Wrapped; } diff --git a/packages/react/src/profiler.tsx b/packages/react/src/profiler.tsx index dca938e38044..5008ffdc0010 100644 --- a/packages/react/src/profiler.tsx +++ b/packages/react/src/profiler.tsx @@ -169,9 +169,7 @@ function withProfiler

>( // Copy over static methods from Wrapped component to Profiler HOC // See: https://reactjs.org/docs/higher-order-components.html#static-methods-must-be-copied-over - // Need to set type to any because of hoist-non-react-statics typing - // eslint-disable-next-line @typescript-eslint/no-explicit-any - hoistNonReactStatics(Wrapped, WrappedComponent as any); + hoistNonReactStatics(Wrapped, WrappedComponent); return Wrapped; } diff --git a/packages/react/src/reactrouter.tsx b/packages/react/src/reactrouter.tsx index 5b5efbcec650..3adf59326012 100644 --- a/packages/react/src/reactrouter.tsx +++ b/packages/react/src/reactrouter.tsx @@ -32,7 +32,7 @@ export type RouteConfig = { [propName: string]: unknown; path?: string | string[]; exact?: boolean; - component?: React.JSX.Element; + component?: JSX.Element; routes?: RouteConfig[]; }; @@ -244,8 +244,7 @@ export function withSentryRouting

, R extends React }; WrappedRoute.displayName = `sentryRoute(${componentDisplayName})`; - // Need to set type to any because of hoist-non-react-statics typing - hoistNonReactStatics(WrappedRoute, Route as any); + hoistNonReactStatics(WrappedRoute, Route); // @ts-expect-error Setting more specific React Component typing for `R` generic above // will break advanced type inference done by react router params: // https://github.com/DefinitelyTyped/DefinitelyTyped/blob/13dc4235c069e25fe7ee16e11f529d909f9f3ff8/types/react-router/index.d.ts#L154-L164 diff --git a/packages/react/src/types.ts b/packages/react/src/types.ts index 0374b9e47e80..ffb961bbfe3b 100644 --- a/packages/react/src/types.ts +++ b/packages/react/src/types.ts @@ -1,6 +1,5 @@ // Disabling `no-explicit-any` for the whole file as `any` has became common requirement. /* eslint-disable @typescript-eslint/no-explicit-any */ -import type { JSX } from 'react'; export type Action = 'PUSH' | 'REPLACE' | 'POP'; diff --git a/packages/react/test/errorboundary.test.tsx b/packages/react/test/errorboundary.test.tsx index 42a4c8b69be4..d185dc8a9647 100644 --- a/packages/react/test/errorboundary.test.tsx +++ b/packages/react/test/errorboundary.test.tsx @@ -26,16 +26,16 @@ jest.mock('@sentry/browser', () => { }; }); -function Boo({ title }: { title: string }): React.JSX.Element { +function Boo({ title }: { title: string }): JSX.Element { throw new Error(title); } -function Bam(): React.JSX.Element { +function Bam(): JSX.Element { const [title] = useState('boom'); return ; } -function EffectSpyFallback({ error }: { error: unknown }): React.JSX.Element { +function EffectSpyFallback({ error }: { error: unknown }): JSX.Element { const [counter, setCounter] = useState(0); React.useEffect(() => { @@ -50,10 +50,10 @@ function EffectSpyFallback({ error }: { error: unknown }): React.JSX.Element { } interface TestAppProps extends ErrorBoundaryProps { - errorComp?: React.JSX.Element; + errorComp?: JSX.Element; } -const TestApp: React.FC = ({ children, errorComp, ...props }) => { +const TestApp: React.FC = ({ children, errorComp, ...props }): any => { const customErrorComp = errorComp || ; const [isError, setError] = React.useState(false); return ( @@ -282,7 +282,7 @@ describe('ErrorBoundary', () => { it('does not set cause if non Error objected is thrown', () => { const TestAppThrowingString: React.FC = ({ children, ...props }) => { const [isError, setError] = React.useState(false); - function StringBam(): React.JSX.Element { + function StringBam(): JSX.Element { throw 'bam'; } return ( @@ -333,7 +333,7 @@ describe('ErrorBoundary', () => { it('handles when `error.cause` is nested', () => { const mockOnError = jest.fn(); - function CustomBam(): React.JSX.Element { + function CustomBam(): JSX.Element { const firstError = new Error('bam'); const secondError = new Error('bam2'); const thirdError = new Error('bam3'); @@ -378,7 +378,7 @@ describe('ErrorBoundary', () => { it('handles when `error.cause` is recursive', () => { const mockOnError = jest.fn(); - function CustomBam(): React.JSX.Element { + function CustomBam(): JSX.Element { const firstError = new Error('bam'); const secondError = new Error('bam2'); // @ts-expect-error Need to set cause on error diff --git a/packages/react/test/reactrouterv3.test.tsx b/packages/react/test/reactrouterv3.test.tsx index d2c289a31abe..c207ead3aab3 100644 --- a/packages/react/test/reactrouterv3.test.tsx +++ b/packages/react/test/reactrouterv3.test.tsx @@ -62,7 +62,7 @@ jest.mock('@sentry/core', () => { describe('browserTracingReactRouterV3', () => { const routes = ( -

{children}
}> +
{children}
}>
Home
} />
About
} />
Features
} /> diff --git a/yarn.lock b/yarn.lock index 182bb280a80e..522f1ef1a8be 100644 --- a/yarn.lock +++ b/yarn.lock @@ -8833,7 +8833,7 @@ dependencies: "@types/react" "*" -"@types/react@*", "@types/react@>=16.9.0": +"@types/react@*", "@types/react@17.0.3", "@types/react@>=16.9.0": version "17.0.3" resolved "https://registry.yarnpkg.com/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== @@ -8842,14 +8842,6 @@ "@types/scheduler" "*" csstype "^3.0.2" -"@types/react@^18.0.0": - version "18.3.3" - resolved "https://registry.yarnpkg.com/@types/react/-/react-18.3.3.tgz#9679020895318b0915d7a3ab004d92d33375c45f" - integrity sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw== - dependencies: - "@types/prop-types" "*" - csstype "^3.0.2" - "@types/resolve@1.17.1": version "1.17.1" resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.17.1.tgz#3afd6ad8967c77e4376c598a82ddd58f46ec45d6"