From 6227c13e01f0735be21ae4fe507af2cd8f6db86c Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 12 Feb 2025 18:20:16 +0100 Subject: [PATCH] test(react-router): Add basic e2e test (#15369) Adds a basic react-router framework test to the e2e suite. closes https://github.com/getsentry/sentry-javascript/issues/15187 --- dev-packages/e2e-tests/package.json | 2 +- .../react-router-7-framework/.gitignore | 32 ++++ .../react-router-7-framework/.npmrc | 2 + .../react-router-7-framework/app/app.css | 6 + .../app/entry.client.tsx | 23 +++ .../app/entry.server.tsx | 73 +++++++++ .../react-router-7-framework/app/root.tsx | 69 +++++++++ .../react-router-7-framework/app/routes.ts | 18 +++ .../app/routes/errors/client-action.tsx | 18 +++ .../app/routes/errors/client-loader.tsx | 16 ++ .../app/routes/errors/client-param.tsx | 17 +++ .../app/routes/errors/client.tsx | 15 ++ .../app/routes/errors/server-action.tsx | 18 +++ .../app/routes/errors/server-loader.tsx | 16 ++ .../app/routes/home.tsx | 9 ++ .../app/routes/performance/dynamic-param.tsx | 12 ++ .../app/routes/performance/index.tsx | 3 + .../app/routes/performance/static.tsx | 3 + .../react-router-7-framework/instrument.mjs | 9 ++ .../react-router-7-framework/package.json | 58 ++++++++ .../playwright.config.mjs | 8 + .../public/favicon.ico | Bin 0 -> 15086 bytes .../react-router.config.ts | 7 + .../start-event-proxy.mjs | 6 + .../tests/constants.ts | 1 + .../tests/errors/errors.client.test.ts | 138 ++++++++++++++++++ .../tests/errors/errors.server.test.ts | 98 +++++++++++++ .../tests/performance/pageload.client.test.ts | 83 +++++++++++ .../performance/performance.server.test.ts | 111 ++++++++++++++ .../react-router-7-framework/tsconfig.json | 25 ++++ .../react-router-7-framework/vite.config.ts | 6 + 31 files changed, 901 insertions(+), 1 deletion(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/app.css create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/root.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-action.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-loader.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-param.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-action.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-loader.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/home.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/dynamic-param.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/static.tsx create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/constants.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/vite.config.ts diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 8a0eb8010128..039a9eb8760b 100644 --- a/dev-packages/e2e-tests/package.json +++ b/dev-packages/e2e-tests/package.json @@ -16,7 +16,7 @@ "clean": "rimraf tmp node_modules && yarn clean:test-applications && yarn clean:pnpm", "ci:build-matrix": "ts-node ./lib/getTestMatrix.ts", "ci:build-matrix-optional": "ts-node ./lib/getTestMatrix.ts --optional=true", - "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,pnpm-lock.yaml,.last-run.json,test-results}", + "clean:test-applications": "rimraf --glob test-applications/**/{node_modules,dist,build,.next,.nuxt,.sveltekit,.react-router,pnpm-lock.yaml,.last-run.json,test-results}", "clean:pnpm": "pnpm store prune" }, "devDependencies": { diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/.gitignore b/dev-packages/e2e-tests/test-applications/react-router-7-framework/.gitignore new file mode 100644 index 000000000000..ebb991370034 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/.gitignore @@ -0,0 +1,32 @@ +# 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 + +# react router +.react-router diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/.npmrc b/dev-packages/e2e-tests/test-applications/react-router-7-framework/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/.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-router-7-framework/app/app.css b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/app.css new file mode 100644 index 000000000000..b31c3a9d0ddf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/app.css @@ -0,0 +1,6 @@ +html, +body { + @media (prefers-color-scheme: dark) { + color-scheme: dark; + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx new file mode 100644 index 000000000000..2200fcea97c3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx @@ -0,0 +1,23 @@ +import * as Sentry from '@sentry/react-router'; +import { StrictMode, startTransition } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { HydratedRouter } from 'react-router/dom'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + // todo: get this from env + dsn: 'https://username@domain/123', + tunnel: `http://localhost:3031/`, // proxy server + integrations: [Sentry.browserTracingIntegration()], + tracesSampleRate: 1.0, + tracePropagationTargets: [/^\//], +}); + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx new file mode 100644 index 000000000000..faa62bd97197 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.server.tsx @@ -0,0 +1,73 @@ +import { PassThrough } from 'node:stream'; + +import { createReadableStreamFromReadable } from '@react-router/node'; +import * as Sentry from '@sentry/react-router'; +import { isbot } from 'isbot'; +import type { RenderToPipeableStreamOptions } from 'react-dom/server'; +import { renderToPipeableStream } from 'react-dom/server'; +import type { AppLoadContext, EntryContext } from 'react-router'; +import { ServerRouter } from 'react-router'; +const ABORT_DELAY = 5_000; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + routerContext: EntryContext, + loadContext: AppLoadContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + let userAgent = request.headers.get('user-agent'); + + // Ensure requests from bots and SPA Mode renders wait for all content to load before responding + // https://react.dev/reference/react-dom/server/renderToPipeableStream#waiting-for-all-content-to-load-for-crawlers-and-static-generation + let readyOption: keyof RenderToPipeableStreamOptions = + (userAgent && isbot(userAgent)) || routerContext.isSpaMode ? 'onAllReady' : 'onShellReady'; + + const { pipe, abort } = renderToPipeableStream(, { + [readyOption]() { + shellRendered = true; + const body = new PassThrough(); + const stream = createReadableStreamFromReadable(body); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(stream, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + // Log streaming rendering errors from inside the shell. Don't log + // errors encountered during initial shell rendering since they'll + // reject and get logged in handleDocumentRequest. + if (shellRendered) { + console.error(error); + } + }, + }); + + setTimeout(abort, ABORT_DELAY); + }); +} + +import { type HandleErrorFunction } from 'react-router'; + +export const handleError: HandleErrorFunction = (error, { request }) => { + // React Router may abort some interrupted requests, don't log those + if (!request.signal.aborted) { + Sentry.captureException(error); + + // make sure to still log the error so you can see it + console.error(error); + } +}; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/root.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/root.tsx new file mode 100644 index 000000000000..227c08f7730c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/root.tsx @@ -0,0 +1,69 @@ +import * as Sentry from '@sentry/react-router'; +import { Links, Meta, Outlet, Scripts, ScrollRestoration, isRouteErrorResponse } from 'react-router'; +import type { Route } from './+types/root'; +import stylesheet from './app.css?url'; + +export const links: Route.LinksFunction = () => [ + { rel: 'preconnect', href: 'https://fonts.googleapis.com' }, + { + rel: 'preconnect', + href: 'https://fonts.gstatic.com', + crossOrigin: 'anonymous', + }, + { + rel: 'stylesheet', + href: 'https://fonts.googleapis.com/css2?family=Inter:ital,opsz,wght@0,14..32,100..900;1,14..32,100..900&display=swap', + }, + { rel: 'stylesheet', href: stylesheet }, +]; + +export function Layout({ children }: { children: React.ReactNode }) { + return ( + + + + + + + + + {children} + + + + + ); +} + +export default function App() { + return ; +} + +export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) { + let message = 'Oops!'; + let details = 'An unexpected error occurred.'; + let stack: string | undefined; + + if (isRouteErrorResponse(error)) { + message = error.status === 404 ? '404' : 'Error'; + details = error.status === 404 ? 'The requested page could not be found.' : error.statusText || details; + } else if (error && error instanceof Error) { + Sentry.captureException(error); + if (import.meta.env.DEV) { + details = error.message; + stack = error.stack; + } + } + + return ( +
+

{message}

+

{details}

+ {stack && ( +
+          {stack}
+        
+ )} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts new file mode 100644 index 000000000000..bb7472366681 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts @@ -0,0 +1,18 @@ +import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes'; + +export default [ + index('routes/home.tsx'), + ...prefix('errors', [ + route('client', 'routes/errors/client.tsx'), + route('client/:client-param', 'routes/errors/client-param.tsx'), + route('client-loader', 'routes/errors/client-loader.tsx'), + route('server-loader', 'routes/errors/server-loader.tsx'), + route('client-action', 'routes/errors/client-action.tsx'), + route('server-action', 'routes/errors/server-action.tsx'), + ]), + ...prefix('performance', [ + index('routes/performance/index.tsx'), + route('with/:param', 'routes/performance/dynamic-param.tsx'), + route('static', 'routes/performance/static.tsx'), + ]), +] satisfies RouteConfig; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-action.tsx new file mode 100644 index 000000000000..d3b2d08eef2e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-action.tsx @@ -0,0 +1,18 @@ +import { Form } from 'react-router'; + +export function clientAction() { + throw new Error('Madonna mia! Che casino nella Client Action!'); +} + +export default function ClientActionErrorPage() { + return ( +
+

Client Error Action Page

+
+ +
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-loader.tsx new file mode 100644 index 000000000000..72d9e62a99dc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-loader.tsx @@ -0,0 +1,16 @@ +import type { Route } from './+types/server-loader'; + +export function clientLoader() { + throw new Error('¡Madre mía del client loader!'); + return { data: 'sad' }; +} + +export default function ClientLoaderErrorPage({ loaderData }: Route.ComponentProps) { + const { data } = loaderData; + return ( +
+

Client Loader Error Page

+
{data}
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-param.tsx new file mode 100644 index 000000000000..a2e423391f03 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-param.tsx @@ -0,0 +1,17 @@ +import type { Route } from './+types/client-param'; + +export default function ClientErrorParamPage({ params }: Route.ComponentProps) { + return ( +
+

Client Error Param Page

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client.tsx new file mode 100644 index 000000000000..190074a5ef09 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client.tsx @@ -0,0 +1,15 @@ +export default function ClientErrorPage() { + return ( +
+

Client Error Page

+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-action.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-action.tsx new file mode 100644 index 000000000000..863c320f3557 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-action.tsx @@ -0,0 +1,18 @@ +import { Form } from 'react-router'; + +export function action() { + throw new Error('Madonna mia! Che casino nella Server Action!'); +} + +export default function ServerActionErrorPage() { + return ( +
+

Server Error Action Page

+
+ +
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-loader.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-loader.tsx new file mode 100644 index 000000000000..cb777686d540 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/server-loader.tsx @@ -0,0 +1,16 @@ +import type { Route } from './+types/server-loader'; + +export function loader() { + throw new Error('¡Madre mía del server!'); + return { data: 'sad' }; +} + +export default function ServerLoaderErrorPage({ loaderData }: Route.ComponentProps) { + const { data } = loaderData; + return ( +
+

Server Error Page

+
{data}
+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/home.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/home.tsx new file mode 100644 index 000000000000..4498e7a0d017 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/home.tsx @@ -0,0 +1,9 @@ +import type { Route } from './+types/home'; + +export function meta({}: Route.MetaArgs) { + return [{ title: 'New React Router App' }, { name: 'description', content: 'Welcome to React Router!' }]; +} + +export default function Home() { + return
home
; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/dynamic-param.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/dynamic-param.tsx new file mode 100644 index 000000000000..39cf7bd5dbf6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/dynamic-param.tsx @@ -0,0 +1,12 @@ +import type { Route } from './+types/dynamic-param'; + +export default function DynamicParamPage({ params }: Route.ComponentProps) { + const { param } = params; + + return ( +
+

Dynamic Parameter Page

+

The parameter value is: {param}

+
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/index.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/index.tsx new file mode 100644 index 000000000000..9d55975e61a5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/index.tsx @@ -0,0 +1,3 @@ +export default function PerformancePage() { + return

Performance Page

; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/static.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/static.tsx new file mode 100644 index 000000000000..3dea24381fdc --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/performance/static.tsx @@ -0,0 +1,3 @@ +export default function StaticPage() { + return

Static Page

; +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs new file mode 100644 index 000000000000..70768dd2a6b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/react-router'; + +Sentry.init({ + // todo: grab from env + dsn: 'https://username@domain/123', + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, + tunnel: `http://localhost:3031/`, // proxy server +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json new file mode 100644 index 000000000000..cdd96f39569e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json @@ -0,0 +1,58 @@ +{ + "name": "react-router-7-framework", + "version": "0.1.0", + "type": "module", + "private": true, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-router": "^7.1.5", + "@react-router/node": "^7.1.5", + "@react-router/serve": "^7.1.5", + "@sentry/react-router": "latest || *", + "isbot": "^5.1.17" + }, + "devDependencies": { + "@types/react": "18.3.1", + "@types/react-dom": "18.3.1", + "@types/node": "^20", + "@react-router/dev": "^7.1.5", + "@playwright/test": "~1.50.0", + "@sentry-internal/test-utils": "link:../../../test-utils", + "typescript": "^5.6.3", + "vite": "^5.4.11" + }, + "scripts": { + "build": "react-router build", + "dev": "NODE_OPTIONS='--import ./instrument.mjs' react-router dev", + "start": "NODE_OPTIONS='--import ./instrument.mjs' react-router-serve ./build/server/index.js", + "proxy": "node start-event-proxy.mjs", + "typecheck": "react-router typegen && tsc", + "clean": "npx rimraf node_modules pnpm-lock.yaml", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:ts && pnpm test:playwright", + "test:ts": "pnpm typecheck", + "test:playwright": "playwright 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" + ] + }, + "volta": { + "extends": "../../package.json" + } +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/playwright.config.mjs new file mode 100644 index 000000000000..3ed5721107a7 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: `PORT=3030 pnpm start`, + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/public/favicon.ico b/dev-packages/e2e-tests/test-applications/react-router-7-framework/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..5dbdfcddcb14182535f6d32d1c900681321b1aa3 GIT binary patch literal 15086 zcmeI33v3ic7{|AFEmuJ-;v>ep_G*NPi6KM`qNryCe1PIJ8siIN1WZ(7qVa)RVtmC% z)Ch?tN+afMKm;5@rvorJk zcXnoOc4q51HBQnQH_jn!cAg&XI1?PlX>Kl^k8qq0;zkha`kY$Fxt#=KNJAE9CMdpW zqr4#g8`nTw191(+H4xW8Tmyru2I^3=J1G3emPxkPXA=3{vvuvse_WWSshqaqls^-m zgB7q8&Vk*aYRe?sn$n53dGH#%3y%^vxv{pL*-h0Z4bmb_(k6{FL7HWIz(V*HT#IcS z-wE{)+0x1U!RUPt3gB97%p}@oHxF4|6S*+Yw=_tLtxZ~`S=z6J?O^AfU>7qOX`JNBbV&8+bO0%@fhQitKIJ^O^ zpgIa__qD_y07t@DFlBJ)8SP_#^j{6jpaXt{U%=dx!qu=4u7^21lWEYHPPY5U3TcoQ zX_7W+lvZi>TapNk_X>k-KO%MC9iZp>1E`N34gHKd9tK&){jq2~7OsJ>!G0FzxQFw6G zm&Vb(2#-T|rM|n3>uAsG_hnbvUKFf3#ay@u4uTzia~NY%XgCHfx4^To4BDU@)HlV? z@EN=g^ymETa1sQK{kRwyE4Ax8?wT&GvaG@ASO}{&a17&^v`y z!oPdiSiia^oov(Z)QhG2&|FgE{M9_4hJROGbnj>#$~ZF$-G^|zPj*QApltKe?;u;uKHJ~-V!=VLkg7Kgct)l7u39f@%VG8e3f$N-B zAu3a4%ZGf)r+jPAYCSLt73m_J3}p>}6Tx0j(wg4vvKhP!DzgiWANiE;Ppvp}P2W@m z-VbYn+NXFF?6ngef5CfY6ZwKnWvNV4z6s^~yMXw2i5mv}jC$6$46g?G|CPAu{W5qF zDobS=zb2ILX9D827g*NtGe5w;>frjanY{f)hrBP_2ehBt1?`~ypvg_Ot4x1V+43P@Ve8>qd)9NX_jWdLo`Zfy zoeam9)@Dpym{4m@+LNxXBPjPKA7{3a&H+~xQvr>C_A;7=JrfK~$M2pCh>|xLz>W6SCs4qC|#V`)# z)0C|?$o>jzh<|-cpf

K7osU{Xp5PG4-K+L2G=)c3f&}H&M3wo7TlO_UJjQ-Oq&_ zjAc9=nNIYz{c3zxOiS5UfcE1}8#iI4@uy;$Q7>}u`j+OU0N<*Ezx$k{x_27+{s2Eg z`^=rhtIzCm!_UcJ?Db~Lh-=_))PT3{Q0{Mwdq;0>ZL%l3+;B&4!&xm#%HYAK|;b456Iv&&f$VQHf` z>$*K9w8T+paVwc7fLfMlhQ4)*zL_SG{~v4QR;IuX-(oRtYAhWOlh`NLoX0k$RUYMi z2Y!bqpdN}wz8q`-%>&Le@q|jFw92ErW-hma-le?S z-@OZt2EEUm4wLsuEMkt4zlyy29_3S50JAcQHTtgTC{P~%-mvCTzrjXOc|{}N`Cz`W zSj7CrXfa7lcsU0J(0uSX6G`54t^7}+OLM0n(|g4waOQ}bd3%!XLh?NX9|8G_|06Ie zD5F1)w5I~!et7lA{G^;uf7aqT`KE&2qx9|~O;s6t!gb`+zVLJyT2T)l*8l(j literal 0 HcmV?d00001 diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts new file mode 100644 index 000000000000..73b647e4eea6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/react-router.config.ts @@ -0,0 +1,7 @@ +import type { Config } from '@react-router/dev/config'; + +export default { + ssr: true, + // todo: check why this messes up client tracing in tests + // prerender: ['/performance/static'], +} satisfies Config; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/start-event-proxy.mjs new file mode 100644 index 000000000000..7a8110ee5ccb --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'react-router-7-framework', +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/constants.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/constants.ts new file mode 100644 index 000000000000..3f70e5327bd6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/constants.ts @@ -0,0 +1 @@ +export const APP_NAME = 'react-router-7-framework'; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.client.test.ts new file mode 100644 index 000000000000..d6c80924c121 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.client.test.ts @@ -0,0 +1,138 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test.describe('client-side errors', () => { + const errorMessage = '¡Madre mía!'; + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === errorMessage; + }); + + await page.goto(`/errors/client`); + await page.locator('#throw-on-click').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: errorMessage, + mechanism: { + handled: false, + }, + }, + ], + }, + transaction: '/errors/client', + request: { + url: expect.stringContaining('errors/client'), + headers: expect.any(Object), + }, + level: 'error', + platform: 'javascript', + environment: 'qa', + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.react-router', + version: expect.any(String), + }, + tags: { runtime: 'browser' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + breadcrumbs: [ + { + category: 'ui.click', + message: 'body > div > button#throw-on-click', + }, + ], + }); + }); + + test('captures error thrown on click from a parameterized route', async ({ page }) => { + const errorMessage = '¡Madre mía de churros!'; + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === errorMessage; + }); + + await page.goto('/errors/client/churros'); + await page.locator('#throw-on-click').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: '¡Madre mía de churros!', + mechanism: { + handled: false, + }, + }, + ], + }, + // todo: should be '/errors/client/:client-param' + transaction: '/errors/client/churros', + }); + }); + + test('captures error thrown in a clientLoader', async ({ page }) => { + const errorMessage = '¡Madre mía del client loader!'; + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === errorMessage; + }); + + await page.goto('/errors/client-loader'); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: errorMessage, + mechanism: { + handled: true, + }, + }, + ], + }, + transaction: '/errors/client-loader', + }); + }); + + test('captures error thrown in a clientAction', async ({ page }) => { + const errorMessage = 'Madonna mia! Che casino nella Client Action!'; + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === errorMessage; + }); + + await page.goto('/errors/client-action'); + await page.locator('#submit').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: errorMessage, + mechanism: { + handled: true, + }, + }, + ], + }, + transaction: '/errors/client-action', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.server.test.ts new file mode 100644 index 000000000000..d702f8cee597 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.server.test.ts @@ -0,0 +1,98 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test.describe('server-side errors', () => { + test('captures error thrown in server loader', async ({ page }) => { + const errorMessage = '¡Madre mía del server!'; + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === errorMessage; + }); + + await page.goto(`/errors/server-loader`); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: errorMessage, + mechanism: { + handled: true, + }, + }, + ], + }, + // todo: should be 'GET /errors/server-loader' + transaction: 'GET *', + request: { + url: expect.stringContaining('errors/server-loader'), + headers: expect.any(Object), + }, + level: 'error', + platform: 'node', + environment: 'qa', + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.react-router', + version: expect.any(String), + }, + tags: { runtime: 'node' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }); + }); + + test('captures error thrown in server action', async ({ page }) => { + const errorMessage = 'Madonna mia! Che casino nella Server Action!'; + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === errorMessage; + }); + + await page.goto(`/errors/server-action`); + await page.locator('#submit').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: errorMessage, + mechanism: { + handled: true, + }, + }, + ], + }, + // todo: should be 'POST /errors/server-action' + transaction: 'POST *', + request: { + url: expect.stringContaining('errors/server-action'), + headers: expect.any(Object), + }, + level: 'error', + platform: 'node', + environment: 'qa', + sdk: { + integrations: expect.any(Array), + name: 'sentry.javascript.react-router', + version: expect.any(String), + }, + tags: { runtime: 'node' }, + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + }, + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts new file mode 100644 index 000000000000..c53494c723b4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts @@ -0,0 +1,83 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test.describe('client - pageload performance', () => { + test('should send pageload transaction', async ({ page }) => { + const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { + return transactionEvent.transaction === '/performance'; + }); + + await page.goto(`/performance`); + + const transaction = await txPromise; + + expect(transaction).toMatchObject({ + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'auto.pageload.browser', + 'sentry.op': 'pageload', + 'sentry.source': 'url', + }, + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + transaction: '/performance', + type: 'transaction', + transaction_info: { source: 'url' }, + measurements: expect.any(Object), + platform: 'javascript', + request: { + url: expect.stringContaining('/performance'), + headers: expect.any(Object), + }, + event_id: expect.any(String), + environment: 'qa', + sdk: { + integrations: expect.arrayContaining([expect.any(String)]), + name: 'sentry.javascript.react-router', + version: expect.any(String), + packages: [ + { name: 'npm:@sentry/react-router', version: expect.any(String) }, + { name: 'npm:@sentry/browser', version: expect.any(String) }, + ], + }, + tags: { runtime: 'browser' }, + }); + }); + + // todo: this page is currently not prerendered (see react-router.config.ts) + test('should send pageload transaction for prerendered pages', async ({ page }) => { + const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { + return transactionEvent.transaction === '/performance/static'; + }); + + await page.goto(`/performance/static`); + + const transaction = await txPromise; + + expect(transaction).toMatchObject({ + transaction: '/performance/static', + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.origin': 'auto.pageload.browser', + 'sentry.op': 'pageload', + 'sentry.source': 'url', + }, + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts new file mode 100644 index 000000000000..f080d01064ea --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts @@ -0,0 +1,111 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { APP_NAME } from '../constants'; + +test.describe('servery - performance', () => { + test('should send server transaction on pageload', async ({ page }) => { + const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { + // todo: should be GET /performance + return transactionEvent.transaction === 'GET *'; + }); + + await page.goto(`/performance`); + + const transaction = await txPromise; + + expect(transaction).toMatchObject({ + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.otel.http', + }, + }, + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + // todo: should be GET /performance + transaction: 'GET *', + type: 'transaction', + transaction_info: { source: 'route' }, + platform: 'node', + request: { + url: expect.stringContaining('/performance'), + headers: expect.any(Object), + }, + event_id: expect.any(String), + environment: 'qa', + sdk: { + integrations: expect.arrayContaining([expect.any(String)]), + name: 'sentry.javascript.react-router', + version: expect.any(String), + packages: [ + { name: 'npm:@sentry/react-router', version: expect.any(String) }, + { name: 'npm:@sentry/node', version: expect.any(String) }, + ], + }, + tags: { + runtime: 'node', + }, + }); + }); + + test('should send server transaction on parameterized route', async ({ page }) => { + const txPromise = waitForTransaction(APP_NAME, async transactionEvent => { + // todo: should be GET /performance/with/:param + return transactionEvent.transaction === 'GET *'; + }); + + await page.goto(`/performance/with/some-param`); + + const transaction = await txPromise; + + expect(transaction).toMatchObject({ + contexts: { + trace: { + span_id: expect.any(String), + trace_id: expect.any(String), + data: { + 'sentry.op': 'http.server', + 'sentry.origin': 'auto.http.otel.http', + 'sentry.source': 'route', + }, + op: 'http.server', + origin: 'auto.http.otel.http', + }, + }, + spans: expect.any(Array), + start_timestamp: expect.any(Number), + timestamp: expect.any(Number), + // todo: should be GET /performance/with/:param + transaction: 'GET *', + type: 'transaction', + transaction_info: { source: 'route' }, + platform: 'node', + request: { + url: expect.stringContaining('/performance/with/some-param'), + headers: expect.any(Object), + }, + event_id: expect.any(String), + environment: 'qa', + sdk: { + integrations: expect.arrayContaining([expect.any(String)]), + name: 'sentry.javascript.react-router', + version: expect.any(String), + packages: [ + { name: 'npm:@sentry/react-router', version: expect.any(String) }, + { name: 'npm:@sentry/node', version: expect.any(String) }, + ], + }, + tags: { + runtime: 'node', + }, + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tsconfig.json b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tsconfig.json new file mode 100644 index 000000000000..4b7a52f6bddf --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tsconfig.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2022"], + "types": ["node", "vite/client"], + "target": "ES2022", + "module": "ES2022", + "moduleResolution": "bundler", + "jsx": "react-jsx", + "rootDirs": [".", "./.react-router/types"], + "baseUrl": ".", + + "esModuleInterop": true, + "verbatimModuleSyntax": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strict": true + }, + "include": [ + "**/*", + "**/.server/**/*", + "**/.client/**/*", + ".react-router/types/**/*", + ], +} diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/vite.config.ts new file mode 100644 index 000000000000..68ba30d69397 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/vite.config.ts @@ -0,0 +1,6 @@ +import { reactRouter } from '@react-router/dev/vite'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + plugins: [reactRouter()], +});