diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7609e25a856f..cd93a8a054dd 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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', diff --git a/.gitignore b/.gitignore index 3cc2319ea8a9..4f7805cacdfe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,7 @@ node_modules/ packages/*/package-lock.json dev-packages/*/package-lock.json package-lock.json +.yarn # build and test # SDK builds diff --git a/dev-packages/e2e-tests/README.md b/dev-packages/e2e-tests/README.md index 76fa6370d154..2b9896de0dd4 100644 --- a/dev-packages/e2e-tests/README.md +++ b/dev-packages/e2e-tests/README.md @@ -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: @@ -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 @@ -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 ` (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 + ``` diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.eslintrc.cjs b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.eslintrc.cjs new file mode 100644 index 000000000000..e9494d6185e2 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.eslintrc.cjs @@ -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, + }, + }, + ], +}; diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.gitignore b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.gitignore new file mode 100644 index 000000000000..d3479de50914 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.gitignore @@ -0,0 +1,7 @@ +node_modules + +/.cache +/build +.env + +tests/events diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.npmrc b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/.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-fastify-vite/README.md b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/README.md new file mode 100644 index 000000000000..a0420c582646 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/README.md @@ -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. diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/entry.client.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/entry.client.tsx new file mode 100644 index 000000000000..5b5cfaa3e261 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/entry.client.tsx @@ -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, + + + , + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/entry.server.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/entry.server.tsx new file mode 100644 index 000000000000..15a867a8e1f5 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/entry.server.tsx @@ -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( + , + { + 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-fastify-vite/app/root.tsx b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/root.tsx new file mode 100644 index 000000000000..cc6e95dfd25c --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/create-remix-app-fastify-vite/app/root.tsx @@ -0,0 +1,98 @@ +import { 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'; +// eslint-disable-next-line import/no-unresolved +import '#app/styles/global.css'; + +export const loader = () => { + return json({ + ENV: { + SENTRY_DSN: process.env.E2E_TEST_DSN, + }, + }); +}; + +export const meta = ({ data }: SentryMetaArgs>) => { + return [ + { + env: JSON.stringify(data.ENV), + }, + { + name: 'sentry-trace', + content: data.sentryTrace, + }, + { + name: 'baggage', + content: data.sentryBaggage, + }, + ]; +}; + +export function ErrorBoundary() { + const error = useRouteError(); + const eventId = captureRemixErrorBoundaryError(error); + const { ENV } = useLoaderData(); + + return ( + + + + +