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 Fastify server adapter #11261

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
e3397e0
Add fastify types to polymorphics
rustworthy Mar 23, 2024
63ddafb
Add fastify branches to requestdata parsing
rustworthy Mar 23, 2024
7b2b971
Re-use logic for Express and Fastify adapters
rustworthy Mar 23, 2024
4231514
Ingore .yarn dir
rustworthy Mar 23, 2024
578f1da
Turn 'SupportedFramework' to string enum
rustworthy Mar 23, 2024
0a5dfa8
Check for '__sentry_fastify_wrapped__' prop in 'instrumentServer' mod
rustworthy Mar 24, 2024
3231279
chore: add remix-fastify adapter to remix/test package
rustworthy Apr 6, 2024
f5f1671
chore: untrack .yarn dir
rustworthy Apr 6, 2024
72309fe
chore: rm generated d.ts for gatsby
rustworthy Apr 6, 2024
d294533
chore: add 'Fastify' to integrations tests
rustworthy Apr 6, 2024
01b3d4b
chore: use form body parser in fastify app
rustworthy Apr 6, 2024
ec96d48
chore: use fastify types
rustworthy Apr 6, 2024
ea61973
Revert "chore: use fastify types"
rustworthy Apr 10, 2024
18e567f
Rm fastify-remix from integrtions tests
rustworthy Apr 10, 2024
a495c7f
chore: add remix-fastify-vite test app
rustworthy Apr 16, 2024
ee6cb16
Setup E2E test application.
onurtemizkan Apr 19, 2024
0cbb034
Upd e2e-tests/README with troubleshooting section
rustworthy Apr 20, 2024
1f8f58f
Add tests for client-side integration
rustworthy Apr 20, 2024
920680d
Fix 'wrapRequestHandler' args order
rustworthy Apr 24, 2024
15a7a0c
Update type hits to honor tsc
rustworthy Apr 25, 2024
3b37721
Clean up, add color-preferece honoring bg
rustworthy Apr 25, 2024
78e4d22
Add authenticated axious client as test helper
rustworthy Apr 25, 2024
f8bcdb2
Add tests pass with released @sentry/remix and fastify engine
rustworthy Apr 26, 2024
fb15ad8
Pipe playwrigth logs to stdout
rustworthy Apr 27, 2024
838de36
Merge branch 'develop' into feat/remix-fastify-adapter
rustworthy Apr 27, 2024
60ba722
Merge branch 'develop' into feat/remix-fastify-adapter
rustworthy Apr 27, 2024
849e6fd
Use inspect and .txt. ext for Sentry server events
rustworthy Apr 27, 2024
a7bcba8
Return reply for Fastify adapter
rustworthy Apr 29, 2024
3f0b5ea
Update GenericRequiestHandler signature
rustworthy Apr 29, 2024
cb6d651
Set transaction name in `wrapRequestHandler`
rustworthy Apr 29, 2024
aeb5cb2
Apply biome fmt fixes
rustworthy Apr 29, 2024
373dffd
Merge branch 'develop' into feat/remix-fastify-adapter
rustworthy Apr 29, 2024
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 @@ -1000,6 +1000,7 @@ jobs:
'create-remix-app-v2',
'create-remix-app-express',
'create-remix-app-express-vite-dev',
'create-remix-app-fastify-vite',
'debug-id-sourcemaps',
# 'esm-loader-node-express-app', # This is currently broken for upstream reasons. See https://github.com/getsentry/sentry-javascript/pull/11338#issuecomment-2025450675
'nextjs-app-dir',
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ node_modules/
packages/*/package-lock.json
dev-packages/*/package-lock.json
package-lock.json
.yarn

# build and test
# SDK builds
Expand Down
26 changes: 23 additions & 3 deletions dev-packages/e2e-tests/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Prerequisites: Docker

- Copy `.env.example` to `.env`
- Fill in auth information in `.env` for an example Sentry project
- The `E2E_TEST_AUTH_TOKEN` must have all the default permissions
- The `E2E_TEST_AUTH_TOKEN` must have all the default permissions (at least `project:read`)
- Run `yarn build:tarball` in the root of the repository

To finally run all of the tests:
Expand Down Expand Up @@ -62,8 +62,8 @@ want to run a canary test, add it to the `canary.yml` workflow.
**An important thing to note:** In the context of the build/test commands the fake test registry is available at
`http://127.0.0.1:4873`. It hosts all of our packages as if they were to be published with the state of the current
branch. This means we can install the packages from this registry via the `.npmrc` configuration as seen above. If you
add Sentry dependencies to your test application, you should set the dependency versions set to `latest || *` in order
for it to work with both regular and prerelease versions:
add Sentry dependencies to your test application, you should set the dependency versions to `latest || *` in order for
it to work with both regular and prerelease versions:

```jsonc
// package.json
Expand Down Expand Up @@ -124,3 +124,23 @@ A standardized frontend test application has the following features:
### Standardized Backend Test Apps

TBD

## Troubleshooting

If you encounter issues installing dependencies or building packages, make sure you:

- are using `volta` as a toolchain manager (you will want to manage your `node` and `yarn` bins with it)
- have not messed up with yarn configuration (see an example [issue](https://github.com/yarnpkg/yarn/issues/4890) and a
possible [solution](https://github.com/yarnpkg/yarn/issues/5259#issuecomment-379769451))
- have tried these steps:
- `yarn build:tarball` (from the monorepo root) to build packages and create an archive per package;
- `pnpm store prune` (from the `e2e-tests` dir) to make sure the latest tarball is published to the test registry;
- `yarn test:e2e` or `yarn test:run <test-app-name>` (from the `e2e-tests` dir) to run the tests.
- have read and followd the [How to set up a new test](#how-to-set-up-a-new-test);
- have set up your `.env` (as per [instructions](#how-to-run))to resemble this:
```sh
E2E_TEST_AUTH_TOKEN=sntryu_549************************************************************b79
E2E_TEST_DSN=https://dce09ec*********************c8ce@o**************36.ingest.us.sentry.io/450***********040
E2E_TEST_SENTRY_ORG_SLUG=however-u-called-your-org
E2E_TEST_SENTRY_TEST_PROJECT=for-example-sentry-js-test-project
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* 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,
},
ignorePatterns: ['!**/.server', '!**/.client'],

// 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,
},
},
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules

/.cache
/build
.env

tests/events
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
@sentry:registry=http://127.0.0.1:4873
@sentry-internal:registry=http://127.0.0.1:4873
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Welcome to Remix + Vite + Fastify + Sentry!

📖 See the [Remix docs](https://remix.run/docs) and the [Remix Vite docs](https://remix.run/docs/en/main/future/vite)
for details on supported features.

## Development

Run the Express server with Vite dev middleware:

```sh
pnpm dev
```

## Deployment

First, build your app for production:

```sh
pnpm build
```

Then run the app in production mode:

```sh
pnpm start
```

Now you'll need to pick a host to deploy it to.

## Sentry Events Inspection

After you have launched the app with `pnpm dev:events`, visit the index page and open your browser's network tab. You
will find there an event sent to `Sentry` and correlated server event will be written to `tests/events` directory.
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
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';
import type { ISentry } from 'types';

Sentry.init({
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: window.ENV.SENTRY_DSN,
integrations: [
Sentry.browserTracingIntegration({
useEffect,
useLocation,
useMatches,
}),
(Sentry as ISentry).replayIntegration(),
],
// 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.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,
<StrictMode>
<RemixBrowser />
</StrictMode>,
);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import fsp from 'node:fs/promises';
import util from 'node:util';
import * as Sentry from '@sentry/remix';

Sentry.init({
tracesSampleRate: 1.0, // Capture 100% of the transactions, reduce in production!
environment: 'qa', // dynamic sampling bias to keep transactions
dsn: process.env.E2E_TEST_DSN,
beforeSendTransaction: async e => {
if (process.env.SNAPSHOT_SERVER_EVENTS) {
// Snapshot events for inspections and e2e tests purposes.
await fsp.writeFile(`tests/events/${e.event_id}.txt`, util.inspect(e, false, null));
}
return e;
},
});

import { PassThrough } from 'node:stream';

import type { AppLoadContext, EntryContext } from '@remix-run/node';
import { createReadableStreamFromReadable } from '@remix-run/node';
import { RemixServer } from '@remix-run/react';
import { isbot } from 'isbot';
import { renderToPipeableStream } from 'react-dom/server';

const ABORT_DELAY = 5_000;

export default function handleRequest(
request: Request,
responseStatusCode: number,
responseHeaders: Headers,
remixContext: EntryContext,
// This is ignored so we can keep it in the template for visibility. Feel
// free to delete this parameter in your app if you're not using it!
// eslint-disable-next-line @typescript-eslint/no-unused-vars
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);
});
}
Loading
Loading