From 39d7148adbd94de17aa74f89751b9d2838f8bb99 Mon Sep 17 00:00:00 2001 From: Onur Temizkan Date: Mon, 10 Jun 2024 16:03:28 +0100 Subject: [PATCH] Add Remix/Express legacy e2e tests. --- .github/workflows/build.yml | 1 + .../.eslintrc.cjs | 79 +++++++ .../.gitignore | 6 + .../create-remix-app-express-legacy/.npmrc | 2 + .../app/entry.client.tsx | 46 +++++ .../app/entry.server.tsx | 141 +++++++++++++ .../app/root.tsx | 80 ++++++++ .../app/routes/_index.tsx | 29 +++ .../app/routes/action-formdata.tsx | 17 ++ .../app/routes/client-error.tsx | 24 +++ .../app/routes/loader-error.tsx | 16 ++ .../app/routes/navigate.tsx | 20 ++ .../app/routes/user.$id.tsx | 3 + .../create-remix-app-express-legacy/env.d.ts | 2 + .../globals.d.ts | 7 + .../instrument.cjs | 14 ++ .../package.json | 57 ++++++ .../playwright.config.mjs | 7 + .../server.mjs | 52 +++++ .../start-event-proxy.mjs | 6 + .../tests/behaviour-client.test.ts | 192 ++++++++++++++++++ .../tests/behaviour-server.test.ts | 146 +++++++++++++ .../tsconfig.json | 22 ++ .../vite.config.ts | 18 ++ .../app/routes/action-formdata.tsx | 6 +- .../create-remix-app-express/server.mjs | 2 +- 26 files changed, 989 insertions(+), 6 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.eslintrc.cjs create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.server.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/root.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/_index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/action-formdata.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/client-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/loader-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/navigate.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/routes/user.$id.tsx create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/env.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/globals.d.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/instrument.cjs create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/package.json create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/server.mjs create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tests/behaviour-server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/vite.config.ts diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index c25417cb5266..62967b801cc0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1001,6 +1001,7 @@ jobs: 'create-remix-app-v2', 'create-remix-app-v2-legacy', 'create-remix-app-express', + 'create-remix-app-express-legacy', 'create-remix-app-express-vite-dev', 'node-express-esm-loader', 'node-express-esm-preload', diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.eslintrc.cjs b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.eslintrc.cjs new file mode 100644 index 000000000000..7adbd6f482f6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.eslintrc.cjs @@ -0,0 +1,79 @@ +/** + * This is intended to be a basic starting point for linting in your app. + * It relies on recommended configs out of the box for simplicity, but you can + * and should modify this configuration to best suit your team's needs. + */ + +/** @type {import('eslint').Linter.Config} */ +module.exports = { + root: true, + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module', + ecmaFeatures: { + jsx: true, + }, + }, + env: { + browser: true, + commonjs: true, + es6: true, + }, + + // Base config + extends: ['eslint:recommended'], + + overrides: [ + // React + { + files: ['**/*.{js,jsx,ts,tsx}'], + plugins: ['react', 'jsx-a11y'], + extends: [ + 'plugin:react/recommended', + 'plugin:react/jsx-runtime', + 'plugin:react-hooks/recommended', + 'plugin:jsx-a11y/recommended', + ], + settings: { + react: { + version: 'detect', + }, + formComponents: ['Form'], + linkComponents: [ + { name: 'Link', linkAttribute: 'to' }, + { name: 'NavLink', linkAttribute: 'to' }, + ], + 'import/resolver': { + typescript: {}, + }, + }, + }, + + // Typescript + { + files: ['**/*.{ts,tsx}'], + plugins: ['@typescript-eslint', 'import'], + parser: '@typescript-eslint/parser', + settings: { + 'import/internal-regex': '^~/', + 'import/resolver': { + node: { + extensions: ['.ts', '.tsx'], + }, + typescript: { + alwaysTryTypes: true, + }, + }, + }, + extends: ['plugin:@typescript-eslint/recommended', 'plugin:import/recommended', 'plugin:import/typescript'], + }, + + // Node + { + files: ['.eslintrc.cjs', 'server.js'], + env: { + node: true, + }, + }, + ], +}; diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.gitignore b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.gitignore new file mode 100644 index 000000000000..3f7bf98da3e1 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.npmrc b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/.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/create-remix-app-express-legacy/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx new file mode 100644 index 000000000000..5ee250101be9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.client.tsx @@ -0,0 +1,46 @@ +import { RemixBrowser, useLocation, useMatches } from '@remix-run/react'; +import * as Sentry from '@sentry/remix'; +import { StrictMode, startTransition, useEffect } from 'react'; +import { hydrateRoot } from 'react-dom/client'; + +Sentry.init({ + environment: 'qa', // dynamic sampling bias to keep transactions + dsn: window.ENV.SENTRY_DSN, + integrations: [ + Sentry.browserTracingIntegration({ + useEffect, + useLocation, + useMatches, + }), + Sentry.replayIntegration(), + ], + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! + 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. + tunnel: 'http://localhost:3031/', // proxy server +}); + +Sentry.addEventProcessor(event => { + if ( + event.type === 'transaction' && + (event.contexts?.trace?.op === 'pageload' || event.contexts?.trace?.op === 'navigation') + ) { + const eventId = event.event_id; + if (eventId) { + window.recordedTransactions = window.recordedTransactions || []; + window.recordedTransactions.push(eventId); + } + } + + return event; +}); + +startTransition(() => { + hydrateRoot( + document, + + + , + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.server.tsx new file mode 100644 index 000000000000..4e1e9e0ba537 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/entry.server.tsx @@ -0,0 +1,141 @@ +import * as Sentry from '@sentry/remix'; + +import { PassThrough } from 'node:stream'; +import * as isbotModule from 'isbot'; + +import type { AppLoadContext, EntryContext } from '@remix-run/node'; +import { createReadableStreamFromReadable } from '@remix-run/node'; +import { installGlobals } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import { renderToPipeableStream } from 'react-dom/server'; + +installGlobals(); + +const ABORT_DELAY = 5_000; + +export const handleError = Sentry.wrapRemixHandleError; + +export default function handleRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, + loadContext: AppLoadContext, +) { + return isBotRequest(request.headers.get('user-agent')) + ? handleBotRequest(request, responseStatusCode, responseHeaders, remixContext) + : handleBrowserRequest(request, responseStatusCode, responseHeaders, remixContext); +} + +// We have some Remix apps in the wild already running with isbot@3 so we need +// to maintain backwards compatibility even though we want new apps to use +// isbot@4. That way, we can ship this as a minor Semver update to @remix-run/dev. +function isBotRequest(userAgent: string | null) { + if (!userAgent) { + return false; + } + + // isbot >= 3.8.0, >4 + if ('isbot' in isbotModule && typeof isbotModule.isbot === 'function') { + return isbotModule.isbot(userAgent); + } + + // isbot < 3.8.0 + if ('default' in isbotModule && typeof isbotModule.default === 'function') { + return isbotModule.default(userAgent); + } + + return false; +} + +function handleBotRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onAllReady() { + 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); + }); +} + +function handleBrowserRequest( + request: Request, + responseStatusCode: number, + responseHeaders: Headers, + remixContext: EntryContext, +) { + return new Promise((resolve, reject) => { + let shellRendered = false; + const { pipe, abort } = renderToPipeableStream( + , + { + onShellReady() { + 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); + }); +} diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/root.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/root.tsx new file mode 100644 index 000000000000..517a37a9d76b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-express-legacy/app/root.tsx @@ -0,0 +1,80 @@ +import { cssBundleHref } from '@remix-run/css-bundle'; +import { LinksFunction, MetaFunction, json } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, + useRouteError, +} from '@remix-run/react'; +import { captureRemixErrorBoundaryError, withSentry } from '@sentry/remix'; +import type { SentryMetaArgs } from '@sentry/remix'; + +export const links: LinksFunction = () => [...(cssBundleHref ? [{ rel: 'stylesheet', href: cssBundleHref }] : [])]; + +export const loader = () => { + return json({ + ENV: { + SENTRY_DSN: process.env.E2E_TEST_DSN, + }, + }); +}; + +export const meta = ({ data }: SentryMetaArgs>) => { + return [ + { + env: data.ENV, + }, + { + name: 'sentry-trace', + content: data.sentryTrace, + }, + { + name: 'baggage', + content: data.sentryBaggage, + }, + ]; +}; + +export function ErrorBoundary() { + const error = useRouteError(); + const eventId = captureRemixErrorBoundaryError(error); + + return ( +
+ ErrorBoundary Error + {eventId} +
+ ); +} + +function App() { + const { ENV } = useLoaderData(); + + return ( + + + + +