From c21bc148b053654a684fb1219884506536cc18fe Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 11 Feb 2025 12:26:00 +0100 Subject: [PATCH 1/7] minimal app setup --- .../react-router-7-framework/.gitignore | 32 ++++++++ .../react-router-7-framework/.npmrc | 2 + .../react-router-7-framework/app/app.css | 12 +++ .../app/entry.client.tsx | 20 +++++ .../app/entry.server.tsx | 73 +++++++++++++++++++ .../react-router-7-framework/app/root.tsx | 69 ++++++++++++++++++ .../react-router-7-framework/app/routes.ts | 3 + .../app/routes/home.tsx | 9 +++ .../react-router-7-framework/instrument.mjs | 6 ++ .../react-router-7-framework/package.json | 59 +++++++++++++++ .../playwright.config.mjs | 8 ++ .../react-router.config.ts | 7 ++ .../start-event-proxy.mjs | 6 ++ .../tests/constants.ts | 1 + .../react-router-7-framework/tsconfig.json | 25 +++++++ .../react-router-7-framework/vite.config.ts | 13 ++++ 16 files changed, 345 insertions(+) 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/home.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/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/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/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..303fe158fcf9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/app.css @@ -0,0 +1,12 @@ +@tailwind base; +@tailwind components; +@tailwind utilities; + +html, +body { + @apply bg-white dark:bg-gray-950; + + @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..8060e243294e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx @@ -0,0 +1,20 @@ +import * as Sentry from '@sentry/react-router'; +import { startTransition, StrictMode } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import { HydratedRouter } from 'react-router/dom'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + 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..24013f657223 --- /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 type { AppLoadContext, EntryContext } from 'react-router'; +import { createReadableStreamFromReadable } from '@react-router/node'; +import { ServerRouter } from 'react-router'; +import { isbot } from 'isbot'; +import type { RenderToPipeableStreamOptions } from 'react-dom/server'; +import { renderToPipeableStream } from 'react-dom/server'; +import * as Sentry from '@sentry/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..d192d4303a0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/root.tsx @@ -0,0 +1,69 @@ +import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; +import * as Sentry from '@sentry/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..205ff3ccb9fd --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes.ts @@ -0,0 +1,3 @@ +import { type RouteConfig, index } from '@react-router/dev/routes'; + +export default [index('routes/home.tsx')] satisfies RouteConfig; 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/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs new file mode 100644 index 000000000000..f5478a90e24f --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs @@ -0,0 +1,6 @@ +import * as Sentry from '@sentry/react-router'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + tracesSampleRate: 1.0, +}); 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..bc921efa6f5a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/package.json @@ -0,0 +1,59 @@ +{ + "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", + "autoprefixer": "^10.4.20", + "postcss": "^8.4.49", + "tailwindcss": "^3.4.15", + "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", + "test": "pnpm typecheck && playwright test", + "typecheck": "react-router typegen && tsc", + "clean": "npx rimraf node_modules .react-router pnpm-lock.yaml", + "test:build": "pnpm 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" + ] + }, + "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..4ca3c24e7fda --- /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: `pnpm start`, + port: 3000, +}); + +export default config; 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..4f9a6ed5228d --- /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 { + // Config options... + // Server-side render by default, to enable SPA mode set this to `false` + ssr: true, +} 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/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..1d30b1a99243 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/vite.config.ts @@ -0,0 +1,13 @@ +import { reactRouter } from '@react-router/dev/vite'; +import autoprefixer from 'autoprefixer'; +import tailwindcss from 'tailwindcss'; +import { defineConfig } from 'vite'; + +export default defineConfig({ + css: { + postcss: { + plugins: [tailwindcss, autoprefixer], + }, + }, + plugins: [reactRouter()], +}); From 18c95988920788d6cecacbb7f3913d62a278c6fe Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 11 Feb 2025 19:19:47 +0100 Subject: [PATCH 2/7] add client error tests --- dev-packages/e2e-tests/package.json | 2 +- .../app/entry.client.tsx | 5 +- .../react-router-7-framework/app/routes.ts | 10 ++- .../app/routes/errors/client-param.tsx | 17 ++++ .../app/routes/errors/client.tsx | 15 ++++ .../react-router-7-framework/instrument.mjs | 2 + .../react-router-7-framework/package.json | 8 +- .../playwright.config.mjs | 4 +- .../public/favicon.ico | Bin 0 -> 15086 bytes .../tests/errors.client.test.ts | 83 ++++++++++++++++++ 10 files changed, 137 insertions(+), 9 deletions(-) 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/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts diff --git a/dev-packages/e2e-tests/package.json b/dev-packages/e2e-tests/package.json index 78c71ba9e1b0..71caa77bed19 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/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/react-router-7-framework/app/entry.client.tsx index 8060e243294e..7e60798ba7cb 100644 --- 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 @@ -4,9 +4,12 @@ import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; Sentry.init({ - dsn: process.env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + // todo: get this from env + dsn: 'https://username@domain/123', integrations: [Sentry.browserTracingIntegration()], tracesSampleRate: 1.0, + tunnel: `http://localhost:3031/`, // proxy server tracePropagationTargets: [/^\//], }); 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 index 205ff3ccb9fd..77244216171f 100644 --- 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 @@ -1,3 +1,9 @@ -import { type RouteConfig, index } from '@react-router/dev/routes'; +import { type RouteConfig, index, prefix, route } from '@react-router/dev/routes'; -export default [index('routes/home.tsx')] satisfies RouteConfig; +export default [ + index('routes/home.tsx'), + ...prefix('errors', [ + route('client', 'routes/errors/client.tsx'), + route('client/:client-param', 'routes/errors/client-param.tsx'), + ]), +] satisfies RouteConfig; 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/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs index f5478a90e24f..0947857ab377 100644 --- 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 @@ -1,6 +1,8 @@ import * as Sentry from '@sentry/react-router'; Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + tunnel: `http://localhost:3031/`, // proxy server dsn: process.env.E2E_TEST_DSN, tracesSampleRate: 1.0, }); 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 index bc921efa6f5a..cfb5e8b920c4 100644 --- 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 @@ -29,11 +29,13 @@ "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", - "test": "pnpm typecheck && playwright test", + "proxy": "node start-event-proxy.mjs", "typecheck": "react-router typegen && tsc", - "clean": "npx rimraf node_modules .react-router pnpm-lock.yaml", + "clean": "npx rimraf node_modules pnpm-lock.yaml", "test:build": "pnpm install && pnpm build", - "test:assert": "pnpm test" + "test:assert": "pnpm test:ts && pnpm test:playwright", + "test:ts": "pnpm typecheck", + "test:playwright": "playwright test" }, "eslintConfig": { "extends": [ 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 index 4ca3c24e7fda..3ed5721107a7 100644 --- 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 @@ -1,8 +1,8 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: `pnpm start`, - port: 3000, + 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/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts new file mode 100644 index 000000000000..ff75db6b0d8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts @@ -0,0 +1,83 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import { APP_NAME } from './constants'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === '¡Madre mía!'; + }); + + await page.goto(`/errors/client`); + await page.locator('#throw-on-click').click(); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: '¡Madre mía!', + 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 errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === '¡Madre mía de churros!'; + }); + + 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', + }); + }); +}); From 1dc13a613ac2c806e2e452cba7a11189c2378396 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Tue, 11 Feb 2025 20:43:30 +0100 Subject: [PATCH 3/7] add test for server error --- .../app/entry.client.tsx | 2 +- .../react-router-7-framework/app/routes.ts | 1 + .../app/routes/errors/server-loader.tsx | 16 ++++++ .../react-router-7-framework/instrument.mjs | 4 +- .../tests/errors.server.test.ts | 50 +++++++++++++++++++ 5 files changed, 71 insertions(+), 2 deletions(-) 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/tests/errors.server.test.ts 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 index 7e60798ba7cb..a2abc6540079 100644 --- 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 @@ -7,9 +7,9 @@ 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, - tunnel: `http://localhost:3031/`, // proxy server tracePropagationTargets: [/^\//], }); 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 index 77244216171f..466522c22c10 100644 --- 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 @@ -5,5 +5,6 @@ export default [ ...prefix('errors', [ route('client', 'routes/errors/client.tsx'), route('client/:client-param', 'routes/errors/client-param.tsx'), + route('server-loader', 'routes/errors/server-loader.tsx'), ]), ] satisfies RouteConfig; 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/instrument.mjs b/dev-packages/e2e-tests/test-applications/react-router-7-framework/instrument.mjs index 0947857ab377..ecf713feed46 100644 --- 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 @@ -3,6 +3,8 @@ import * as Sentry from '@sentry/react-router'; Sentry.init({ environment: 'qa', // dynamic sampling bias to keep transactions tunnel: `http://localhost:3031/`, // proxy server - dsn: process.env.E2E_TEST_DSN, + // todo: grab from env + // dsn: process.env.E2E_TEST_DSN, + dsn: 'https://username@domain/123', tracesSampleRate: 1.0, }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts new file mode 100644 index 000000000000..6496b7c0b581 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts @@ -0,0 +1,50 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; +import { APP_NAME } from './constants'; + +test.describe('server-side errors', () => { + test.only('captures error thrown in server loader', async ({ page }) => { + const errorPromise = waitForError(APP_NAME, async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === '¡Madre mía del server!'; + }); + + await page.goto(`/errors/server-loader`); + + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: '¡Madre mía del server!', + 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), + }, + }, + }); + }); +}); From 2adb1161be226fdf378f6977fcf3772777758874 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 12 Feb 2025 10:05:00 +0100 Subject: [PATCH 4/7] add test for errors in client loader --- .../react-router-7-framework/app/routes.ts | 1 + .../app/routes/errors/client-loader.tsx | 16 +++++++++ .../tests/errors.client.test.ts | 34 +++++++++++++++++-- .../tests/errors.server.test.ts | 2 +- 4 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/app/routes/errors/client-loader.tsx 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 index 466522c22c10..6c3ae0a6defe 100644 --- 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 @@ -5,6 +5,7 @@ export default [ ...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'), ]), ] satisfies RouteConfig; 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/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts index ff75db6b0d8d..6c3b2fa4b6f5 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts @@ -3,9 +3,10 @@ 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 === '¡Madre mía!'; + return errorEvent?.exception?.values?.[0]?.value === errorMessage; }); await page.goto(`/errors/client`); @@ -18,7 +19,7 @@ test.describe('client-side errors', () => { values: [ { type: 'Error', - value: '¡Madre mía!', + value: errorMessage, mechanism: { handled: false, }, @@ -55,8 +56,9 @@ test.describe('client-side errors', () => { }); 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 === '¡Madre mía de churros!'; + return errorEvent?.exception?.values?.[0]?.value === errorMessage; }); await page.goto('/errors/client/churros'); @@ -80,4 +82,30 @@ test.describe('client-side errors', () => { 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', + }); + }); }); diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts index 6496b7c0b581..007e108ef3f9 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts @@ -3,7 +3,7 @@ import { waitForError } from '@sentry-internal/test-utils'; import { APP_NAME } from './constants'; test.describe('server-side errors', () => { - test.only('captures error thrown in server loader', async ({ page }) => { + test('captures error thrown in server loader', async ({ page }) => { const errorPromise = waitForError(APP_NAME, async errorEvent => { return errorEvent?.exception?.values?.[0]?.value === '¡Madre mía del server!'; }); From e6642585712e2377e91ceb6ae7fbd1e57e56fd95 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 12 Feb 2025 10:45:18 +0100 Subject: [PATCH 5/7] more error tests --- .../react-router-7-framework/app/routes.ts | 2 + .../app/routes/errors/client-action.tsx | 18 +++++++ .../app/routes/errors/server-action.tsx | 18 +++++++ .../tests/errors.client.test.ts | 27 ++++++++++ .../tests/errors.server.test.ts | 52 ++++++++++++++++++- 5 files changed, 115 insertions(+), 2 deletions(-) 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/server-action.tsx 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 index 6c3ae0a6defe..b1e54ed49244 100644 --- 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 @@ -7,5 +7,7 @@ export default [ 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'), ]), ] 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/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/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts index 6c3b2fa4b6f5..04c4985cbf8b 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts @@ -108,4 +108,31 @@ test.describe('client-side errors', () => { 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.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts index 007e108ef3f9..43668f489d66 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts @@ -4,8 +4,9 @@ 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 === '¡Madre mía del server!'; + return errorEvent?.exception?.values?.[0]?.value === errorMessage; }); await page.goto(`/errors/server-loader`); @@ -17,7 +18,7 @@ test.describe('server-side errors', () => { values: [ { type: 'Error', - value: '¡Madre mía del server!', + value: errorMessage, mechanism: { handled: true, }, @@ -47,4 +48,51 @@ test.describe('server-side errors', () => { }, }); }); + + 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), + }, + }, + }); + }); }); From 709efc1abd3b4680fcac166ce858af53c1fefec7 Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 12 Feb 2025 13:00:50 +0100 Subject: [PATCH 6/7] performance tests --- .../react-router-7-framework/app/app.css | 6 -- .../app/entry.client.tsx | 2 +- .../app/entry.server.tsx | 6 +- .../react-router-7-framework/app/root.tsx | 2 +- .../react-router-7-framework/app/routes.ts | 5 ++ .../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 | 5 +- .../react-router-7-framework/package.json | 3 - .../react-router.config.ts | 4 +- .../tests/{ => errors}/errors.client.test.ts | 2 +- .../tests/{ => errors}/errors.server.test.ts | 2 +- .../tests/performance/pageload.client.test.ts | 83 +++++++++++++++++++ .../react-router-7-framework/vite.config.ts | 7 -- 15 files changed, 117 insertions(+), 28 deletions(-) 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 rename dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/{ => errors}/errors.client.test.ts (98%) rename dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/{ => errors}/errors.server.test.ts (98%) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/pageload.client.test.ts 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 index 303fe158fcf9..b31c3a9d0ddf 100644 --- 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 @@ -1,11 +1,5 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; - html, body { - @apply bg-white dark:bg-gray-950; - @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 index a2abc6540079..2200fcea97c3 100644 --- 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 @@ -1,5 +1,5 @@ import * as Sentry from '@sentry/react-router'; -import { startTransition, StrictMode } from 'react'; +import { StrictMode, startTransition } from 'react'; import { hydrateRoot } from 'react-dom/client'; import { HydratedRouter } from 'react-router/dom'; 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 index 24013f657223..faa62bd97197 100644 --- 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 @@ -1,12 +1,12 @@ import { PassThrough } from 'node:stream'; -import type { AppLoadContext, EntryContext } from 'react-router'; import { createReadableStreamFromReadable } from '@react-router/node'; -import { ServerRouter } from 'react-router'; +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 * as Sentry from '@sentry/react-router'; +import type { AppLoadContext, EntryContext } from 'react-router'; +import { ServerRouter } from 'react-router'; const ABORT_DELAY = 5_000; export default function handleRequest( 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 index d192d4303a0d..227c08f7730c 100644 --- 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 @@ -1,5 +1,5 @@ -import { isRouteErrorResponse, Links, Meta, Outlet, Scripts, ScrollRestoration } from 'react-router'; 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'; 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 index b1e54ed49244..bb7472366681 100644 --- 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 @@ -10,4 +10,9 @@ export default [ 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/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 index ecf713feed46..70768dd2a6b4 100644 --- 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 @@ -1,10 +1,9 @@ import * as Sentry from '@sentry/react-router'; Sentry.init({ - environment: 'qa', // dynamic sampling bias to keep transactions - tunnel: `http://localhost:3031/`, // proxy server // todo: grab from env - // dsn: process.env.E2E_TEST_DSN, 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 index cfb5e8b920c4..cdd96f39569e 100644 --- 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 @@ -19,9 +19,6 @@ "@react-router/dev": "^7.1.5", "@playwright/test": "~1.50.0", "@sentry-internal/test-utils": "link:../../../test-utils", - "autoprefixer": "^10.4.20", - "postcss": "^8.4.49", - "tailwindcss": "^3.4.15", "typescript": "^5.6.3", "vite": "^5.4.11" }, 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 index 4f9a6ed5228d..73b647e4eea6 100644 --- 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 @@ -1,7 +1,7 @@ import type { Config } from '@react-router/dev/config'; export default { - // Config options... - // Server-side render by default, to enable SPA mode set this to `false` 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/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.client.test.ts similarity index 98% rename from dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts rename to dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.client.test.ts index 04c4985cbf8b..d6c80924c121 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.client.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.client.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; -import { APP_NAME } from './constants'; +import { APP_NAME } from '../constants'; test.describe('client-side errors', () => { const errorMessage = '¡Madre mía!'; diff --git a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.server.test.ts similarity index 98% rename from dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts rename to dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.server.test.ts index 43668f489d66..d702f8cee597 100644 --- a/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors.server.test.ts +++ b/dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/errors/errors.server.test.ts @@ -1,6 +1,6 @@ import { expect, test } from '@playwright/test'; import { waitForError } from '@sentry-internal/test-utils'; -import { APP_NAME } from './constants'; +import { APP_NAME } from '../constants'; test.describe('server-side errors', () => { test('captures error thrown in server loader', async ({ page }) => { 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/vite.config.ts b/dev-packages/e2e-tests/test-applications/react-router-7-framework/vite.config.ts index 1d30b1a99243..68ba30d69397 100644 --- 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 @@ -1,13 +1,6 @@ import { reactRouter } from '@react-router/dev/vite'; -import autoprefixer from 'autoprefixer'; -import tailwindcss from 'tailwindcss'; import { defineConfig } from 'vite'; export default defineConfig({ - css: { - postcss: { - plugins: [tailwindcss, autoprefixer], - }, - }, plugins: [reactRouter()], }); From 17d84d7c5b225d5c363f625a407401200a98959e Mon Sep 17 00:00:00 2001 From: Charly Gomez Date: Wed, 12 Feb 2025 13:11:16 +0100 Subject: [PATCH 7/7] add performance server tests --- .../performance/performance.server.test.ts | 111 ++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 dev-packages/e2e-tests/test-applications/react-router-7-framework/tests/performance/performance.server.test.ts 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', + }, + }); + }); +});