Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(remix): Add Remix 2.x release support. #8940

Merged
merged 15 commits into from
Sep 19, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/** @type {import('eslint').Linter.Config} */
module.exports = {
extends: ['@remix-run/eslint-config', '@remix-run/eslint-config/node'],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
node_modules

/.cache
/build
/public/build
.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://localhost:4873
@sentry-internal:registry=http://localhost:4873
61 changes: 61 additions & 0 deletions packages/e2e-tests/test-applications/create-remix-app-v2/README.md
Original file line number Diff line number Diff line change
@@ -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
```
Original file line number Diff line number Diff line change
@@ -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,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -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(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
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(
<RemixServer context={remixContext} url={request.url} abortDelay={ABORT_DELAY} />,
{
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);
});
}
Original file line number Diff line number Diff line change
@@ -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 (
<div>
<span>ErrorBoundary Error</span>
<span id="event-id">{eventId}</span>
</div>
);
}

function App() {
const { ENV } = useLoaderData();

return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<script
dangerouslySetInnerHTML={{
__html: `window.ENV = ${JSON.stringify(ENV)}`,
}}
/>
<Meta />
<Links />
</head>
<body>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</body>
</html>
);
}

export default withSentry(App);
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import * as Sentry from '@sentry/remix';
import { Link } from '@remix-run/react';

export default function Index() {
return (
<div>
<input
type="button"
value="Capture Exception"
id="exception-button"
onClick={() => {
const eventId = Sentry.captureException(new Error('I am an error!'));
window.capturedExceptionId = eventId;
}}
/>
<Link to="/user/5" id="navigation">
navigate
</Link>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { useState } from 'react';

export default function ErrorBoundaryCapture() {
const [count, setCount] = useState(0);

if (count > 0) {
throw new Error('Sentry React Component Error');
} else {
setTimeout(() => setCount(count + 1), 0);
}

return <div>{count}</div>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { LoaderFunction } from '@remix-run/node';
import { useLoaderData } from '@remix-run/react';

export const loader: LoaderFunction = async ({ params: { id } }) => {
if (id === '-1') {
throw new Error('Unexpected Server Error');
}

return null;
};

export default function LoaderError() {
const data = useLoaderData();

return (
<div>
<h1>{data && data.test ? data.test : 'Not Found'}</h1>
</div>
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export default function User() {
return <div>I am a blank page</div>;
}
Loading