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 @@ + + +
+ + + + +>( 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) => ( @@ -241,7 +197,6 @@ 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
diff --git a/packages/react/src/index.ts b/packages/react/src/index.ts
index ef6627cf0c5e..b0ee93d48677 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 { reactErrorHandler } 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