diff --git a/packages/e2e-tests/run.ts b/packages/e2e-tests/run.ts index a062f51aab6c..658b4ffaa97f 100644 --- a/packages/e2e-tests/run.ts +++ b/packages/e2e-tests/run.ts @@ -17,6 +17,7 @@ async function run(): Promise { const envVarsToInject = { REACT_APP_E2E_TEST_DSN: process.env.E2E_TEST_DSN, + REMIX_APP_E2E_TEST_DSN: process.env.E2E_TEST_DSN, NEXT_PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN, PUBLIC_E2E_TEST_DSN: process.env.E2E_TEST_DSN, BASE_PORT: '27496', // just some random port diff --git a/packages/e2e-tests/test-applications/create-remix-app/.eslintrc.js b/packages/e2e-tests/test-applications/create-remix-app/.eslintrc.js new file mode 100644 index 000000000000..f2faf1470fd8 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/.eslintrc.js @@ -0,0 +1,4 @@ +/** @type {import('eslint').Linter.Config} */ +module.exports = { + extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'], +}; diff --git a/packages/e2e-tests/test-applications/create-remix-app/.gitignore b/packages/e2e-tests/test-applications/create-remix-app/.gitignore new file mode 100644 index 000000000000..3f7bf98da3e1 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/packages/e2e-tests/test-applications/create-remix-app/.npmrc b/packages/e2e-tests/test-applications/create-remix-app/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/.npmrc @@ -0,0 +1,2 @@ +@sentry:registry=http://localhost:4873 +@sentry-internal:registry=http://localhost:4873 diff --git a/packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx b/packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx new file mode 100644 index 000000000000..edb18224d6c6 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/app/entry.client.tsx @@ -0,0 +1,34 @@ +/** + * By default, Remix will handle hydrating your app on the client for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.client + */ + +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import { startTransition, StrictMode, useEffect } from 'react'; +import { hydrateRoot } from 'react-dom/client'; +import * as Sentry from '@sentry/remix'; + +Sentry.init({ + dsn: process.env.REMIX_APP_E2E_TEST_DSN, + integrations: [ + new Sentry.BrowserTracing({ + routingInstrumentation: Sentry.remixRouterInstrumentation(useEffect, useLocation, useMatches), + }), + new Sentry.Replay(), + ], + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + // Session Replay + replaysSessionSampleRate: 0.1, // This sets the sample rate at 10%. You may want to change it to 100% while in development and then sample at a lower rate in production. + replaysOnErrorSampleRate: 1.0, // If you're not already sampling the entire session, change the sample rate to 100% when sampling sessions where errors occur. +}); + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx b/packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx new file mode 100644 index 000000000000..c9975e940e21 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/app/entry.server.tsx @@ -0,0 +1,110 @@ +/** + * By default, Remix will handle generating the HTTP Response for you. + * You are free to delete this file if you'd like to, but if you ever want it revealed again, you can run `npx remix reveal` ✨ + * For more information, see https://remix.run/file-conventions/entry.server + */ + +import { PassThrough } from 'node:stream'; + +import type { AppLoadContext, EntryContext } from '@remix-run/node'; +import { Response } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import isbot from 'isbot'; +import { renderToPipeableStream } from 'react-dom/server'; +import * as Sentry from '@sentry/remix'; + +const ABORT_DELAY = 5_000; + +Sentry.init({ + dsn: process.env.REMIX_APP_E2E_TEST_DSN, + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! +}); + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + loadContext: AppLoadContext, +) { + return isbot(request.headers.get('user-agent')) + ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) + : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + const body = new PassThrough(); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + responseStatusCode = 500; + console.error(error); + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + const body = new PassThrough(); + + responseHeaders.set('Content-Type', 'text/html'); + + resolve( + new Response(body, { + headers: responseHeaders, + status: responseStatusCode, + }), + ); + + pipe(body); + }, + onShellError(error: unknown) { + reject(error); + }, + onError(error: unknown) { + console.error(error); + responseStatusCode = 500; + }, + }, + ); + + setTimeout(abort, ABORT_DELAY); + }); +} diff --git a/packages/e2e-tests/test-applications/create-remix-app/app/root.tsx b/packages/e2e-tests/test-applications/create-remix-app/app/root.tsx new file mode 100644 index 000000000000..0f3d1a16e8a8 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/app/root.tsx @@ -0,0 +1,27 @@ +import { cssBundleHref } from '@remix-run/css-bundle'; +import type { LinksFunction } from '@remix-run/node'; +import { Links, LiveReload, Meta, Outlet, Scripts, ScrollRestoration } from '@remix-run/react'; +import { withSentry } from '@sentry/remix'; + +export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])]; + +function App() { + return ( + + + + + + + + + + + + + + + ); +} + +export default withSentry(App); diff --git a/packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx b/packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx new file mode 100644 index 000000000000..e279d69b17b3 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/app/routes/_index.tsx @@ -0,0 +1,30 @@ +import type { V2_MetaFunction } from '@remix-run/node'; + +export const meta: V2_MetaFunction = () => { + return [{ title: 'New Remix App' }, { name: 'description', content: 'Welcome to Remix!' }]; +}; + +export default function Index() { + return ( + + ); +} diff --git a/packages/e2e-tests/test-applications/create-remix-app/package.json b/packages/e2e-tests/test-applications/create-remix-app/package.json new file mode 100644 index 000000000000..080bc7de0446 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/package.json @@ -0,0 +1,31 @@ +{ + "private": true, + "sideEffects": false, + "scripts": { + "build": "remix build", + "dev": "remix dev", + "start": "remix-serve build", + "typecheck": "tsc" + }, + "dependencies": { + "@sentry/remix": "*", + "@remix-run/css-bundle": "^1.16.1", + "@remix-run/node": "^1.16.1", + "@remix-run/react": "^1.16.1", + "@remix-run/serve": "^1.16.1", + "isbot": "^3.6.8", + "react": "^18.2.0", + "react-dom": "^18.2.0" + }, + "devDependencies": { + "@remix-run/dev": "^1.16.1", + "@remix-run/eslint-config": "^1.16.1", + "@types/react": "^18.0.35", + "@types/react-dom": "^18.0.11", + "eslint": "^8.38.0", + "typescript": "^5.0.4" + }, + "engines": { + "node": ">=14" + } +} diff --git a/packages/e2e-tests/test-applications/create-remix-app/remix.config.js b/packages/e2e-tests/test-applications/create-remix-app/remix.config.js new file mode 100644 index 000000000000..1203199a8d5f --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/remix.config.js @@ -0,0 +1,15 @@ +/** @type {import('@remix-run/dev').AppConfig} */ +module.exports = { + ignoredRouteFiles: ['**/.*'], + // appDirectory: "app", + // assetsBuildDirectory: "public/build", + // serverBuildPath: "build/index.js", + // publicPath: "/build/", + serverModuleFormat: 'cjs', + future: { + v2_errorBoundary: true, + v2_meta: true, + v2_normalizeFormMethod: true, + v2_routeConvention: true, + }, +}; diff --git a/packages/e2e-tests/test-applications/create-remix-app/test-recipe.json b/packages/e2e-tests/test-applications/create-remix-app/test-recipe.json new file mode 100644 index 000000000000..25ab76b0b134 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/test-recipe.json @@ -0,0 +1,6 @@ +{ + "$schema": "../../test-recipe-schema.json", + "testApplicationName": "create-remix-app", + "buildCommand": "pnpm install && pnpm build && pnpm start", + "tests": [] +} diff --git a/packages/e2e-tests/test-applications/create-remix-app/tsconfig.json b/packages/e2e-tests/test-applications/create-remix-app/tsconfig.json new file mode 100644 index 000000000000..20f8a386a6c4 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app/tsconfig.json @@ -0,0 +1,22 @@ +{ + "include": ["remix.env.d.ts", "**/*.ts", "**/*.tsx"], + "compilerOptions": { + "lib": ["DOM", "DOM.Iterable", "ES2019"], + "isolatedModules": true, + "esModuleInterop": true, + "jsx": "react-jsx", + "moduleResolution": "node", + "resolveJsonModule": true, + "target": "ES2019", + "strict": true, + "allowJs": true, + "forceConsistentCasingInFileNames": true, + "baseUrl": ".", + "paths": { + "~/*": ["./app/*"] + }, + + // Remix takes care of building everything in `remix build`. + "noEmit": true + } +}