diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cbc80ec59f33..56f61126a45a 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -815,6 +815,7 @@ jobs: 'create-react-app', 'create-next-app', 'create-remix-app', + 'create-remix-app-v2', 'nextjs-app-dir', 'react-create-hash-router', 'react-router-6-use-routes', diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/.eslintrc.js b/packages/e2e-tests/test-applications/create-remix-app-v2/.eslintrc.js new file mode 100644 index 000000000000..f2faf1470fd8 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/.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-v2/.gitignore b/packages/e2e-tests/test-applications/create-remix-app-v2/.gitignore new file mode 100644 index 000000000000..3f7bf98da3e1 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/.gitignore @@ -0,0 +1,6 @@ +node_modules + +/.cache +/build +/public/build +.env diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc b/packages/e2e-tests/test-applications/create-remix-app-v2/.npmrc new file mode 100644 index 000000000000..c6b3ef9b3eaa --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/.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-v2/README.md b/packages/e2e-tests/test-applications/create-remix-app-v2/README.md new file mode 100644 index 000000000000..54336d746713 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/README.md @@ -0,0 +1,61 @@ +# Welcome to Remix! + +- [Remix Docs](https://remix.run/docs) + +## Development + +From your terminal: + +```sh +npm run dev +``` + +This starts your app in development mode, rebuilding assets on file changes. + +## Deployment + +First, build your app for production: + +```sh +npm run build +``` + +Then run the app in production mode: + +```sh +npm start +``` + +Now you'll need to pick a host to deploy it to. + +### DIY + +If you're familiar with deploying node applications, the built-in Remix app server is production-ready. + +Make sure to deploy the output of `remix build` + +- `build/` +- `public/build/` + +### Using a Template + +When you ran `npx create-remix@latest` there were a few choices for hosting. You can run that again to create a new +project, then copy over relevant code/assets from your current app to the new project that's pre-configured for your +target server. + +Most importantly, this means everything in the `app/` directory, but if you've further customized your current +application outside of there it may also include: + +- Any assets you've added/updated in `public/` +- Any updated versions of root files such as `.eslintrc.js`, etc. + +```sh +cd .. +# create a new project, and pick a pre-configured host +npx create-remix@latest +cd my-new-remix-app +# remove the new project's app (not the old one!) +rm -rf app +# copy your app over +cp -R ../my-old-remix-app/app app +``` diff --git a/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx b/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx new file mode 100644 index 000000000000..605d8e792d23 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.client.tsx @@ -0,0 +1,49 @@ +/** + * 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: window.ENV.SENTRY_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. +}); + +Sentry.addGlobalEventProcessor(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/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx b/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx new file mode 100644 index 000000000000..bce3f38ae7f8 --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/app/entry.server.tsx @@ -0,0 +1,133 @@ +/** + * 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, DataFunctionArgs } from '@remix-run/node'; +import { createReadableStreamFromReadable } from '@remix-run/node'; +import { RemixServer } from '@remix-run/react'; +import { renderToPipeableStream } from 'react-dom/server'; +import * as Sentry from '@sentry/remix'; +import { installGlobals } from '@remix-run/node'; +import isbot from 'isbot'; + +installGlobals(); + +const ABORT_DELAY = 5_000; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production! +}); + +export function handleError(error: unknown, { request }: DataFunctionArgs): void { + Sentry.captureRemixServerException(error, 'remix.server', request); +} + +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) => { + 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/packages/e2e-tests/test-applications/create-remix-app-v2/app/root.tsx b/packages/e2e-tests/test-applications/create-remix-app-v2/app/root.tsx new file mode 100644 index 000000000000..d4b2c07516eb --- /dev/null +++ b/packages/e2e-tests/test-applications/create-remix-app-v2/app/root.tsx @@ -0,0 +1,63 @@ +import { cssBundleHref } from '@remix-run/css-bundle'; +import { json, LinksFunction } from '@remix-run/node'; +import { + Links, + LiveReload, + Meta, + Outlet, + Scripts, + ScrollRestoration, + useLoaderData, + useRouteError, +} from '@remix-run/react'; +import { captureRemixErrorBoundaryError, withSentry } 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 function ErrorBoundary() { + const error = useRouteError(); + const eventId = captureRemixErrorBoundaryError(error); + + return ( +
+ ErrorBoundary Error + {eventId} +
+ ); +} + +function App() { + const { ENV } = useLoaderData(); + + return ( + + + + +