Skip to content

Commit f4e7203

Browse files
authored
chore(e2e): Add react-17 e2e test app (#12778)
Adds a react 17 test app so we can hopefully catch breaking react 17, as we did recently (#12608) with #12204 and #12740.
1 parent 15230f1 commit f4e7203

File tree

15 files changed

+389
-3
lines changed

15 files changed

+389
-3
lines changed

.github/workflows/build.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1017,6 +1017,7 @@ jobs:
10171017
'nextjs-app-dir',
10181018
'nextjs-14',
10191019
'nextjs-15',
1020+
'react-17',
10201021
'react-19',
10211022
'react-create-hash-router',
10221023
'react-router-6-use-routes',
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.js
7+
8+
# testing
9+
/coverage
10+
11+
# production
12+
/build
13+
14+
# misc
15+
.DS_Store
16+
.env.local
17+
.env.development.local
18+
.env.test.local
19+
.env.production.local
20+
21+
npm-debug.log*
22+
yarn-debug.log*
23+
yarn-error.log*
24+
25+
/test-results/
26+
/playwright-report/
27+
/playwright/.cache/
28+
29+
!*.d.ts
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
@sentry:registry=http://127.0.0.1:4873
2+
@sentry-internal:registry=http://127.0.0.1:4873
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
{
2+
"name": "react-17",
3+
"version": "0.1.0",
4+
"private": true,
5+
"dependencies": {
6+
"@sentry/react": "latest || *",
7+
"@types/react": "17.0.2",
8+
"@types/react-dom": "17.0.2",
9+
"react": "17.0.2",
10+
"react-dom": "17.0.2",
11+
"react-router-dom": "~6.3.0",
12+
"react-scripts": "5.0.1",
13+
"typescript": "4.9.5"
14+
},
15+
"scripts": {
16+
"build": "react-scripts build",
17+
"dev": "react-scripts start",
18+
"start": "serve -s build",
19+
"test": "playwright test",
20+
"clean": "npx rimraf node_modules pnpm-lock.yaml",
21+
"test:build": "pnpm install && npx playwright install && pnpm build",
22+
"test:build-ts3.8": "pnpm install && pnpm add typescript@3.8 && npx playwright install && pnpm build",
23+
"test:build-canary": "pnpm install && pnpm add react@canary react-dom@canary && npx playwright install && pnpm build",
24+
"test:assert": "pnpm test"
25+
},
26+
"eslintConfig": {
27+
"extends": [
28+
"react-app",
29+
"react-app/jest"
30+
]
31+
},
32+
"browserslist": {
33+
"production": [
34+
">0.2%",
35+
"not dead",
36+
"not op_mini all"
37+
],
38+
"development": [
39+
"last 1 chrome version",
40+
"last 1 firefox version",
41+
"last 1 safari version"
42+
]
43+
},
44+
"devDependencies": {
45+
"@playwright/test": "^1.44.1",
46+
"@sentry-internal/test-utils": "link:../../../test-utils",
47+
"serve": "14.0.1"
48+
},
49+
"volta": {
50+
"extends": "../../package.json"
51+
}
52+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { getPlaywrightConfig } from '@sentry-internal/test-utils';
2+
3+
const config = getPlaywrightConfig({
4+
startCommand: `pnpm start`,
5+
});
6+
7+
export default config;
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
<!DOCTYPE html>
2+
<html lang="en">
3+
<head>
4+
<meta charset="utf-8" />
5+
<meta name="viewport" content="width=device-width, initial-scale=1" />
6+
<meta name="theme-color" content="#000000" />
7+
<meta name="description" content="Web site created using create-react-app" />
8+
<title>React App</title>
9+
</head>
10+
<body>
11+
<noscript>You need to enable JavaScript to run this app.</noscript>
12+
<div id="root"></div>
13+
<!--
14+
This HTML file is a template.
15+
If you open it directly in the browser, you will see an empty page.
16+
17+
You can add webfonts, meta tags, or analytics to this file.
18+
The build step will place the bundled scripts into the <body> tag.
19+
20+
To begin the development, run `npm start` or `yarn start`.
21+
To create a production bundle, use `npm run build` or `yarn build`.
22+
-->
23+
</body>
24+
</html>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import * as Sentry from '@sentry/react';
2+
import React from 'react';
3+
import ReactDOM from 'react-dom';
4+
import {
5+
BrowserRouter,
6+
Route,
7+
Routes,
8+
createRoutesFromChildren,
9+
matchRoutes,
10+
useLocation,
11+
useNavigationType,
12+
} from 'react-router-dom';
13+
import Index from './pages/Index';
14+
import User from './pages/User';
15+
16+
const replay = Sentry.replayIntegration();
17+
18+
Sentry.init({
19+
environment: 'qa', // dynamic sampling bias to keep transactions
20+
dsn: process.env.REACT_APP_E2E_TEST_DSN,
21+
integrations: [
22+
Sentry.reactRouterV6BrowserTracingIntegration({
23+
useEffect: React.useEffect,
24+
useLocation,
25+
useNavigationType,
26+
createRoutesFromChildren,
27+
matchRoutes,
28+
}),
29+
replay,
30+
],
31+
// We recommend adjusting this value in production, or using tracesSampler
32+
// for finer control
33+
tracesSampleRate: 1.0,
34+
release: 'e2e-test',
35+
36+
// Always capture replays, so we can test this properly
37+
replaysSessionSampleRate: 1.0,
38+
replaysOnErrorSampleRate: 0.0,
39+
40+
tunnel: 'http://localhost:3031',
41+
});
42+
43+
const SentryRoutes = Sentry.withSentryReactRouterV6Routing(Routes);
44+
45+
function App() {
46+
return (
47+
<Sentry.ErrorBoundary>
48+
<BrowserRouter>
49+
<SentryRoutes>
50+
<Route path="/" element={<Index />} />
51+
<Route path="/user/:id" element={<User />} />
52+
</SentryRoutes>
53+
</BrowserRouter>
54+
</Sentry.ErrorBoundary>
55+
);
56+
}
57+
58+
ReactDOM.render(<App />, document.getElementById('root'));
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
2+
import * as React from 'react';
3+
import { Link } from 'react-router-dom';
4+
5+
const Index = () => {
6+
return (
7+
<>
8+
<input
9+
type="button"
10+
value="Capture Exception"
11+
id="exception-button"
12+
onClick={() => {
13+
throw new Error('I am an error!');
14+
}}
15+
/>
16+
<Link to="/user/5" id="navigation">
17+
navigate to user
18+
</Link>
19+
</>
20+
);
21+
};
22+
23+
export default Index;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
// biome-ignore lint/nursery/noUnusedImports: Need React import for JSX
2+
import * as React from 'react';
3+
4+
const User = () => {
5+
return <p>I am a blank page :)</p>;
6+
};
7+
8+
export default User;
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/// <reference types="react-scripts" />
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
import { startEventProxyServer } from '@sentry-internal/test-utils';
2+
3+
startEventProxyServer({
4+
port: 3031,
5+
proxyServerName: 'react-17',
6+
});
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForError, waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('Sends correct error event', async ({ page }) => {
5+
const errorEventPromise = waitForError('react-17', event => {
6+
return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
7+
});
8+
9+
await page.goto('/');
10+
11+
const exceptionButton = page.locator('id=exception-button');
12+
await exceptionButton.click();
13+
14+
const errorEvent = await errorEventPromise;
15+
16+
expect(errorEvent.exception?.values).toHaveLength(1);
17+
expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
18+
19+
expect(errorEvent.request).toEqual({
20+
headers: expect.any(Object),
21+
url: 'http://localhost:3030/',
22+
});
23+
24+
expect(errorEvent.transaction).toEqual('/');
25+
26+
expect(errorEvent.contexts?.trace).toEqual({
27+
trace_id: expect.any(String),
28+
span_id: expect.any(String),
29+
});
30+
});
31+
32+
test('Sets correct transactionName', async ({ page }) => {
33+
const transactionPromise = waitForTransaction('react-17', async transactionEvent => {
34+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
35+
});
36+
37+
const errorEventPromise = waitForError('react-17', event => {
38+
return !event.type && event.exception?.values?.[0]?.value === 'I am an error!';
39+
});
40+
41+
await page.goto('/');
42+
const transactionEvent = await transactionPromise;
43+
44+
// Only capture error once transaction was sent
45+
const exceptionButton = page.locator('id=exception-button');
46+
await exceptionButton.click();
47+
48+
const errorEvent = await errorEventPromise;
49+
50+
expect(errorEvent.exception?.values).toHaveLength(1);
51+
expect(errorEvent.exception?.values?.[0]?.value).toBe('I am an error!');
52+
53+
expect(errorEvent.transaction).toEqual('/');
54+
55+
expect(errorEvent.contexts?.trace).toEqual({
56+
trace_id: transactionEvent.contexts?.trace?.trace_id,
57+
span_id: expect.not.stringContaining(transactionEvent.contexts?.trace?.span_id || ''),
58+
});
59+
});
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { expect, test } from '@playwright/test';
2+
import { waitForEnvelopeItem, waitForTransaction } from '@sentry-internal/test-utils';
3+
4+
test('sends a pageload transaction with a parameterized URL', async ({ page }) => {
5+
const transactionPromise = waitForTransaction('react-17', async transactionEvent => {
6+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
7+
});
8+
9+
await page.goto(`/`);
10+
11+
const rootSpan = await transactionPromise;
12+
13+
expect(rootSpan).toMatchObject({
14+
contexts: {
15+
trace: {
16+
op: 'pageload',
17+
origin: 'auto.pageload.react.reactrouter_v6',
18+
},
19+
},
20+
transaction: '/',
21+
transaction_info: {
22+
source: 'route',
23+
},
24+
});
25+
});
26+
27+
test('sends a navigation transaction with a parameterized URL', async ({ page }) => {
28+
page.on('console', msg => console.log(msg.text()));
29+
const pageloadTxnPromise = waitForTransaction('react-17', async transactionEvent => {
30+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'pageload';
31+
});
32+
33+
const navigationTxnPromise = waitForTransaction('react-17', async transactionEvent => {
34+
return !!transactionEvent?.transaction && transactionEvent.contexts?.trace?.op === 'navigation';
35+
});
36+
37+
await page.goto(`/`);
38+
await pageloadTxnPromise;
39+
40+
const linkElement = page.locator('id=navigation');
41+
42+
const [_, navigationTxn] = await Promise.all([linkElement.click(), navigationTxnPromise]);
43+
44+
expect(navigationTxn).toMatchObject({
45+
contexts: {
46+
trace: {
47+
op: 'navigation',
48+
origin: 'auto.navigation.react.reactrouter_v6',
49+
},
50+
},
51+
transaction: '/user/:id',
52+
transaction_info: {
53+
source: 'route',
54+
},
55+
});
56+
});
57+
58+
test('sends an INP span', async ({ page }) => {
59+
const inpSpanPromise = waitForEnvelopeItem('react-17', item => {
60+
return item[0].type === 'span';
61+
});
62+
63+
await page.goto(`/`);
64+
65+
await page.click('#exception-button');
66+
67+
await page.waitForTimeout(500);
68+
69+
// Page hide to trigger INP
70+
await page.evaluate(() => {
71+
window.dispatchEvent(new Event('pagehide'));
72+
});
73+
74+
const inpSpan = await inpSpanPromise;
75+
76+
expect(inpSpan[1]).toEqual({
77+
data: {
78+
'sentry.origin': 'auto.http.browser.inp',
79+
'sentry.op': 'ui.interaction.click',
80+
release: 'e2e-test',
81+
environment: 'qa',
82+
transaction: '/',
83+
'sentry.exclusive_time': expect.any(Number),
84+
replay_id: expect.any(String),
85+
},
86+
description: 'body > div#root > input#exception-button[type="button"]',
87+
op: 'ui.interaction.click',
88+
parent_span_id: expect.any(String),
89+
span_id: expect.any(String),
90+
start_timestamp: expect.any(Number),
91+
timestamp: expect.any(Number),
92+
trace_id: expect.any(String),
93+
origin: 'auto.http.browser.inp',
94+
exclusive_time: expect.any(Number),
95+
measurements: { inp: { unit: 'millisecond', value: expect.any(Number) } },
96+
segment_id: expect.any(String),
97+
});
98+
});

0 commit comments

Comments
 (0)