From a76f243e1c529b1ae8bb658f382ab2bce5409770 Mon Sep 17 00:00:00 2001 From: Sigrid Huemer <32902192+s1gr1d@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:32:32 +0100 Subject: [PATCH] feat(v8/solidstart): Add `withSentry` wrapper for SolidStart config (#15135) Backport of https://github.com/getsentry/sentry-javascript/pull/14862 and https://github.com/getsentry/sentry-javascript/pull/14863 --------- Co-authored-by: Andrei Borza --- CHANGELOG.md | 44 ++++ .../solidstart-dynamic-import/.gitignore | 46 ++++ .../solidstart-dynamic-import/.npmrc | 2 + .../solidstart-dynamic-import/README.md | 45 ++++ .../solidstart-dynamic-import/app.config.ts | 11 + .../solidstart-dynamic-import/package.json | 37 +++ .../playwright.config.mjs | 8 + .../solidstart-dynamic-import/post_build.sh | 8 + .../public/favicon.ico | Bin 0 -> 664 bytes .../solidstart-dynamic-import/src/app.tsx | 22 ++ .../src/entry-client.tsx | 18 ++ .../src/entry-server.tsx | 21 ++ .../src/instrument.server.ts | 9 + .../src/routes/back-navigation.tsx | 9 + .../src/routes/client-error.tsx | 15 ++ .../src/routes/error-boundary.tsx | 64 +++++ .../src/routes/index.tsx | 31 +++ .../src/routes/server-error.tsx | 17 ++ .../src/routes/users/[id].tsx | 21 ++ .../start-event-proxy.mjs | 6 + .../tests/errorboundary.test.ts | 92 +++++++ .../tests/errors.client.test.ts | 30 +++ .../tests/errors.server.test.ts | 30 +++ .../tests/performance.client.test.ts | 95 +++++++ .../tests/performance.server.test.ts | 55 ++++ .../solidstart-dynamic-import/tsconfig.json | 19 ++ .../vitest.config.ts | 10 + .../solidstart-top-level-import/.gitignore | 46 ++++ .../solidstart-top-level-import/.npmrc | 2 + .../solidstart-top-level-import/README.md | 45 ++++ .../solidstart-top-level-import/app.config.ts | 11 + .../solidstart-top-level-import/package.json | 37 +++ .../playwright.config.mjs | 8 + .../solidstart-top-level-import/post_build.sh | 8 + .../public/favicon.ico | Bin 0 -> 664 bytes .../solidstart-top-level-import/src/app.tsx | 22 ++ .../src/entry-client.tsx | 18 ++ .../src/entry-server.tsx | 21 ++ .../src/instrument.server.ts | 9 + .../src/routes/back-navigation.tsx | 9 + .../src/routes/client-error.tsx | 15 ++ .../src/routes/error-boundary.tsx | 64 +++++ .../src/routes/index.tsx | 31 +++ .../src/routes/server-error.tsx | 17 ++ .../src/routes/users/[id].tsx | 21 ++ .../start-event-proxy.mjs | 6 + .../tests/errorboundary.test.ts | 90 +++++++ .../tests/errors.client.test.ts | 30 +++ .../tests/errors.server.test.ts | 30 +++ .../tests/performance.client.test.ts | 95 +++++++ .../tests/performance.server.test.ts | 55 ++++ .../solidstart-top-level-import/tsconfig.json | 19 ++ .../vitest.config.ts | 10 + packages/solidstart/.eslintrc.js | 2 +- .../src/config/addInstrumentation.ts | 182 +++++++++++++ packages/solidstart/src/config/index.ts | 1 + packages/solidstart/src/config/types.ts | 16 ++ packages/solidstart/src/config/utils.ts | 82 ++++++ packages/solidstart/src/config/withSentry.ts | 76 ++++++ .../wrapServerEntryWithDynamicImport.ts | 245 ++++++++++++++++++ packages/solidstart/src/index.server.ts | 1 + packages/solidstart/src/index.types.ts | 1 + .../src/vite/buildInstrumentationFile.ts | 55 ++++ .../src/vite/sentrySolidStartVite.ts | 39 ++- packages/solidstart/src/vite/types.ts | 57 +++- .../test/config/addInstrumentation.test.ts | 222 ++++++++++++++++ .../solidstart/test/config/withSentry.test.ts | 152 +++++++++++ .../test/vite/buildInstrumentation.test.ts | 130 ++++++++++ .../test/vite/sentrySolidStartVite.test.ts | 13 +- 69 files changed, 2751 insertions(+), 7 deletions(-) create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/app.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/app.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts create mode 100644 packages/solidstart/src/config/addInstrumentation.ts create mode 100644 packages/solidstart/src/config/index.ts create mode 100644 packages/solidstart/src/config/types.ts create mode 100644 packages/solidstart/src/config/utils.ts create mode 100644 packages/solidstart/src/config/withSentry.ts create mode 100644 packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts create mode 100644 packages/solidstart/src/vite/buildInstrumentationFile.ts create mode 100644 packages/solidstart/test/config/addInstrumentation.test.ts create mode 100644 packages/solidstart/test/config/withSentry.test.ts create mode 100644 packages/solidstart/test/vite/buildInstrumentation.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 3cf42845dbbf..6f6fe751c3aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,50 @@ - "You miss 100 percent of the chances you don't take. — Wayne Gretzky" — Michael Scott +- **feat(solidstart): Default to `--import` setup and add `autoInjectServerSentry` ([#14862](https://github.com/getsentry/sentry-javascript/pull/14862))** + +To enable the SolidStart SDK, wrap your SolidStart Config with `withSentry`. The `sentrySolidStartVite` plugin is now automatically +added by `withSentry` and you can pass the Sentry build-time options like this: + +```js +import { defineConfig } from '@solidjs/start/config'; +import { withSentry } from '@sentry/solidstart'; + +export default defineConfig( + withSentry( + { + /* Your SolidStart config options... */ + }, + { + // Options for setting up source maps + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + }, + ), +); +``` + +With the `withSentry` wrapper, the Sentry server config should not be added to the `public` directory anymore. +Add the Sentry server config in `src/instrument.server.ts`. Then, the server config will be placed inside the server build output as `instrument.server.mjs`. + +Now, there are two options to set up the SDK: + +1. **(recommended)** Provide an `--import` CLI flag to the start command like this (path depends on your server setup): + `node --import ./.output/server/instrument.server.mjs .output/server/index.mjs` +2. Add `autoInjectServerSentry: 'top-level-import'` and the Sentry config will be imported at the top of the server entry (comes with tracing limitations) + ```js + withSentry( + { + /* Your SolidStart config options... */ + }, + { + // Optional: Install Sentry with a top-level import + autoInjectServerSentry: 'top-level-import', + }, + ); + ``` + ## 8.51.0 ### Important Changes diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore new file mode 100644 index 000000000000..a51ed3c20c8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.gitignore @@ -0,0 +1,46 @@ + +dist +.solid +.output +.vercel +.netlify +.vinxi + +# Environment +.env +.env*.local + +# dependencies +/node_modules +/.pnp +.pnp.js + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# testing +/coverage + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/.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/solidstart-dynamic-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md new file mode 100644 index 000000000000..9a141e9c2f0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/README.md @@ -0,0 +1,45 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add +it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## Testing + +Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some +helpful custom matchers. + +To run them, simply start: + +```sh +npm test +``` + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts new file mode 100644 index 000000000000..f41b1cb186ef --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/app.config.ts @@ -0,0 +1,11 @@ +import { withSentry } from '@sentry/solidstart'; +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig( + withSentry( + {}, + { + autoInjectServerSentry: 'experimental_dynamic-import', + }, + ), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json new file mode 100644 index 000000000000..62393e038dce --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/package.json @@ -0,0 +1,37 @@ +{ + "name": "solidstart-dynamic-import-e2e-testapp", + "version": "0.0.0", + "scripts": { + "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", + "dev": "vinxi dev", + "build": "vinxi build && sh ./post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "type": "module", + "dependencies": { + "@sentry/solidstart": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.13.4", + "@solidjs/start": "^1.0.2", + "@solidjs/testing-library": "^0.8.7", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/user-event": "^14.5.2", + "@vitest/ui": "^1.5.0", + "jsdom": "^24.0.0", + "solid-js": "1.8.17", + "typescript": "^5.4.5", + "vinxi": "^0.4.0", + "vite": "^5.4.10", + "vite-plugin-solid": "^2.10.2", + "vitest": "^1.5.0" + }, + "overrides": { + "@vercel/nft": "0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs new file mode 100644 index 000000000000..395acfc282f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: 'pnpm preview', + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh new file mode 100644 index 000000000000..6ed67c9afb8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/post_build.sh @@ -0,0 +1,8 @@ +# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved. + +# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules` +# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm +# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is +# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher). +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules +cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fb282da0719ef6ab4c1732df93be6216b0d85520 GIT binary patch literal 664 zcmV;J0%!e+P)m9ebk1R zejT~~6f_`?;`cEd!+`7(hw@%%2;?RN8gX-L?z6cM( zKoG@&w+0}f@Pfvwc+deid)qgE!L$ENKYjViZC_Zcr>L(`2oXUT8f0mRQ(6-=HN_Ai zeBBEz3WP+1Cw`m!49Wf!MnZzp5bH8VkR~BcJ1s-j90TAS2Yo4j!J|KodxYR%3Numw zA?gq6e`5@!W~F$_De3yt&uspo&2yLb$(NwcPPI-4LGc!}HdY%jfq@AFs8LiZ4k(p} zZ!c9o+qbWYs-Mg zgdyTALzJX&7QXHdI_DPTFL33;w}88{e6Zk)MX0kN{3DX9uz#O_L58&XRH$Nvvu;fO zf&)7@?C~$z1K<>j0ga$$MIg+5xN;eQ?1-CA=`^Y169@Ab6!vcaNP=hxfKN%@Ly^R* zK1iv*s1Yl6_dVyz8>ZqYhz6J4|3fQ@2LQeX@^%W(B~8>=MoEmBEGGD1;gHXlpX>!W ym)!leA2L@`cpb^hy)P75=I!`pBYxP7<2VfQ3j76qLgzIA0000 ( + + SolidStart - with Vitest + {props.children} + + )} + > + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx new file mode 100644 index 000000000000..11087fbb5918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-client.tsx @@ -0,0 +1,18 @@ +// @refresh reload +import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; +import { StartClient, mount } from '@solidjs/start/client'; + +Sentry.init({ + // We can't use env variables here, seems like they are stripped + // out in production builds. + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + integrations: [solidRouterBrowserTracingIntegration()], + tunnel: 'http://localhost:3031/', // proxy server + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + debug: !!import.meta.env.DEBUG, +}); + +mount(() => , document.getElementById('app')!); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx new file mode 100644 index 000000000000..276935366318 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { StartServer, createHandler } from '@solidjs/start/server'; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts new file mode 100644 index 000000000000..3dd5d8933b7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/instrument.server.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/solidstart'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server + debug: !!process.env.DEBUG, +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx new file mode 100644 index 000000000000..ddd970944bf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/back-navigation.tsx @@ -0,0 +1,9 @@ +import { A } from '@solidjs/router'; + +export default function BackNavigation() { + return ( + + User 6 + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx new file mode 100644 index 000000000000..5e405e8c4e40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/client-error.tsx @@ -0,0 +1,15 @@ +export default function ClientErrorPage() { + return ( +
+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx new file mode 100644 index 000000000000..b22607667e7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/error-boundary.tsx @@ -0,0 +1,64 @@ +import * as Sentry from '@sentry/solidstart'; +import type { ParentProps } from 'solid-js'; +import { ErrorBoundary, createSignal, onMount } from 'solid-js'; + +const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary); + +const [count, setCount] = createSignal(1); +const [caughtError, setCaughtError] = createSignal(false); + +export default function ErrorBoundaryTestPage() { + return ( + + {caughtError() && ( + + )} +
+
+ +
+
+
+ ); +} + +function Throw(props: { error: string }) { + onMount(() => { + throw new Error(props.error); + }); + return null; +} + +function SampleErrorBoundary(props: ParentProps) { + return ( + ( +
+

Error Boundary Fallback

+
+ {error.message} +
+ +
+ )} + > + {props.children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx new file mode 100644 index 000000000000..9a0b22cc38c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/index.tsx @@ -0,0 +1,31 @@ +import { A } from '@solidjs/router'; + +export default function Home() { + return ( + <> +

Welcome to Solid Start

+

+ Visit docs.solidjs.com/solid-start to read the documentation +

+ + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx new file mode 100644 index 000000000000..05dce5e10a56 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/server-error.tsx @@ -0,0 +1,17 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + throw new Error('Error thrown from Solid Start E2E test app server route'); + + return { prefecture: 'Kanagawa' }; + }); +}; + +export default function ServerErrorPage() { + const data = createAsync(() => getPrefecture()); + + return
Prefecture: {data()?.prefecture}
; +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx new file mode 100644 index 000000000000..22abd3ba8803 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/src/routes/users/[id].tsx @@ -0,0 +1,21 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync, useParams } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + return { prefecture: 'Ehime' }; + }); +}; +export default function User() { + const params = useParams(); + const userData = createAsync(() => getPrefecture()); + + return ( +
+ User ID: {params.id} +
+ Prefecture: {userData()?.prefecture} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs new file mode 100644 index 000000000000..343e434e030b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'solidstart-dynamic-import', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts new file mode 100644 index 000000000000..599b5c121455 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errorboundary.test.ts @@ -0,0 +1,92 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('captures an exception', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + // The first page load causes a hydration error on the dev server sometimes - a reload works around this + await page.reload(); + await page.locator('#caughtErrorBtn').click(); + const errorEvent = await errorEventPromise; + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); + +test('captures a second exception after resetting the boundary', async ({ page }) => { + const firstErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const firstErrorEvent = await firstErrorEventPromise; + + expect(firstErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); + + const secondErrorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.locator('#errorBoundaryResetBtn').click(); + await page.locator('#caughtErrorBtn').click(); + const secondErrorEvent = await secondErrorEventPromise; + + expect(secondErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..3a1b3ad4b812 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.client.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('solidstart-dynamic-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Uncaught error thrown from Solid Start E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + transaction: '/client-error', + }); + expect(error.transaction).toEqual('/client-error'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..7ef5cd0e07de --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/errors.server.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures server action error', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-dynamic-import', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route'; + }); + + await page.goto(`/server-error`); + + const error = await errorEventPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Solid Start E2E test app server route', + mechanism: { + type: 'solidstart', + handled: false, + }, + }, + ], + }, + transaction: 'GET /server-error', + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts new file mode 100644 index 000000000000..63f97d519cf8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.client.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await transactionPromise; + + expect(pageloadTransaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); + +test('sends a navigation transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await page.locator('#navLink').click(); + const navigationTransaction = await transactionPromise; + + expect(navigationTransaction).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/5', + transaction_info: { + source: 'url', + }, + }); +}); + +test('updates the transaction when using the back button', async ({ page }) => { + // Solid Router sends a `-1` navigation when using the back button. + // The sentry solidRouterBrowserTracingIntegration tries to update such + // transactions with the proper name once the `useLocation` hook triggers. + const navigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/back-navigation`); + await page.locator('#navLink').click(); + const navigationTxn = await navigationTxnPromise; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/6', + transaction_info: { + source: 'url', + }, + }); + + const backNavigationTxnPromise = waitForTransaction('solidstart-dynamic-import', async transactionEvent => { + return ( + transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goBack(); + const backNavigationTxn = await backNavigationTxnPromise; + + expect(backNavigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/back-navigation', + transaction_info: { + source: 'url', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts new file mode 100644 index 000000000000..c300014bf012 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tests/performance.server.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => { + return transactionEvent?.transaction === 'GET /users/6'; + }); + + await page.goto('/users/6'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); + +test('sends a server action transaction on client navigation', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-dynamic-import', transactionEvent => { + return transactionEvent?.transaction === 'POST getPrefecture'; + }); + + await page.goto('/'); + await page.locator('#navLink').click(); + await page.waitForURL('/users/5'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json new file mode 100644 index 000000000000..6f11292cc5d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts new file mode 100644 index 000000000000..6c2b639dc300 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-dynamic-import/vitest.config.ts @@ -0,0 +1,10 @@ +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + resolve: { + conditions: ['development', 'browser'], + }, + envPrefix: 'PUBLIC_', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore new file mode 100644 index 000000000000..a51ed3c20c8d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.gitignore @@ -0,0 +1,46 @@ + +dist +.solid +.output +.vercel +.netlify +.vinxi + +# Environment +.env +.env*.local + +# dependencies +/node_modules +/.pnp +.pnp.js + +# IDEs and editors +/.idea +.project +.classpath +*.launch +.settings/ + +# Temp +gitignore + +# testing +/coverage + +# misc +.DS_Store +.env.local +.env.development.local +.env.test.local +.env.production.local + +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +/test-results/ +/playwright-report/ +/playwright/.cache/ + +!*.d.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.npmrc new file mode 100644 index 000000000000..070f80f05092 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/.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/solidstart-top-level-import/README.md b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md new file mode 100644 index 000000000000..9a141e9c2f0d --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/README.md @@ -0,0 +1,45 @@ +# SolidStart + +Everything you need to build a Solid project, powered by [`solid-start`](https://start.solidjs.com); + +## Creating a project + +```bash +# create a new project in the current directory +npm init solid@latest + +# create a new project in my-app +npm init solid@latest my-app +``` + +## Developing + +Once you've created a project and installed dependencies with `npm install` (or `pnpm install` or `yarn`), start a +development server: + +```bash +npm run dev + +# or start the server and open the app in a new browser tab +npm run dev -- --open +``` + +## Building + +Solid apps are built with _presets_, which optimise your project for deployment to different environments. + +By default, `npm run build` will generate a Node app that you can run with `npm start`. To use a different preset, add +it to the `devDependencies` in `package.json` and specify in your `app.config.js`. + +## Testing + +Tests are written with `vitest`, `@solidjs/testing-library` and `@testing-library/jest-dom` to extend expect with some +helpful custom matchers. + +To run them, simply start: + +```sh +npm test +``` + +## This project was created with the [Solid CLI](https://solid-cli.netlify.app) diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts new file mode 100644 index 000000000000..e4e73e9fc570 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/app.config.ts @@ -0,0 +1,11 @@ +import { withSentry } from '@sentry/solidstart'; +import { defineConfig } from '@solidjs/start/config'; + +export default defineConfig( + withSentry( + {}, + { + autoInjectServerSentry: 'top-level-import', + }, + ), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json new file mode 100644 index 000000000000..3df1995d6354 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/package.json @@ -0,0 +1,37 @@ +{ + "name": "solidstart-top-level-import-e2e-testapp", + "version": "0.0.0", + "scripts": { + "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", + "dev": "vinxi dev", + "build": "vinxi build && sh ./post_build.sh", + "preview": "HOST=localhost PORT=3030 vinxi start", + "test:prod": "TEST_ENV=production playwright test", + "test:build": "pnpm install && pnpm build", + "test:assert": "pnpm test:prod" + }, + "type": "module", + "dependencies": { + "@sentry/solidstart": "latest || *" + }, + "devDependencies": { + "@playwright/test": "^1.44.1", + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.13.4", + "@solidjs/start": "^1.0.2", + "@solidjs/testing-library": "^0.8.7", + "@testing-library/jest-dom": "^6.4.2", + "@testing-library/user-event": "^14.5.2", + "@vitest/ui": "^1.5.0", + "jsdom": "^24.0.0", + "solid-js": "1.8.17", + "typescript": "^5.4.5", + "vinxi": "^0.4.0", + "vite": "^5.4.10", + "vite-plugin-solid": "^2.10.2", + "vitest": "^1.5.0" + }, + "overrides": { + "@vercel/nft": "0.27.4" + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs new file mode 100644 index 000000000000..395acfc282f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/playwright.config.mjs @@ -0,0 +1,8 @@ +import { getPlaywrightConfig } from '@sentry-internal/test-utils'; + +const config = getPlaywrightConfig({ + startCommand: 'pnpm preview', + port: 3030, +}); + +export default config; diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh new file mode 100644 index 000000000000..6ed67c9afb8a --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/post_build.sh @@ -0,0 +1,8 @@ +# TODO: Investigate the need for this script periodically and remove once these modules are correctly resolved. + +# This script copies `import-in-the-middle` and `@sentry/solidstart` from the E2E test project root `node_modules` +# to the nitro server build output `node_modules` as these are not properly resolved in our yarn workspace/pnpm +# e2e structure. Some files like `hook.mjs` and `@sentry/solidstart/solidrouter.server.js` are missing. This is +# not reproducible in an external project (when pinning `@vercel/nft` to `v0.27.0` and higher). +cp -r node_modules/.pnpm/import-in-the-middle@1.*/node_modules/import-in-the-middle .output/server/node_modules +cp -rL node_modules/@sentry/solidstart .output/server/node_modules/@sentry diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..fb282da0719ef6ab4c1732df93be6216b0d85520 GIT binary patch literal 664 zcmV;J0%!e+P)m9ebk1R zejT~~6f_`?;`cEd!+`7(hw@%%2;?RN8gX-L?z6cM( zKoG@&w+0}f@Pfvwc+deid)qgE!L$ENKYjViZC_Zcr>L(`2oXUT8f0mRQ(6-=HN_Ai zeBBEz3WP+1Cw`m!49Wf!MnZzp5bH8VkR~BcJ1s-j90TAS2Yo4j!J|KodxYR%3Numw zA?gq6e`5@!W~F$_De3yt&uspo&2yLb$(NwcPPI-4LGc!}HdY%jfq@AFs8LiZ4k(p} zZ!c9o+qbWYs-Mg zgdyTALzJX&7QXHdI_DPTFL33;w}88{e6Zk)MX0kN{3DX9uz#O_L58&XRH$Nvvu;fO zf&)7@?C~$z1K<>j0ga$$MIg+5xN;eQ?1-CA=`^Y169@Ab6!vcaNP=hxfKN%@Ly^R* zK1iv*s1Yl6_dVyz8>ZqYhz6J4|3fQ@2LQeX@^%W(B~8>=MoEmBEGGD1;gHXlpX>!W ym)!leA2L@`cpb^hy)P75=I!`pBYxP7<2VfQ3j76qLgzIA0000 ( + + SolidStart - with Vitest + {props.children} + + )} + > + + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx new file mode 100644 index 000000000000..11087fbb5918 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-client.tsx @@ -0,0 +1,18 @@ +// @refresh reload +import * as Sentry from '@sentry/solidstart'; +import { solidRouterBrowserTracingIntegration } from '@sentry/solidstart/solidrouter'; +import { StartClient, mount } from '@solidjs/start/client'; + +Sentry.init({ + // We can't use env variables here, seems like they are stripped + // out in production builds. + dsn: 'https://public@dsn.ingest.sentry.io/1337', + environment: 'qa', // dynamic sampling bias to keep transactions + integrations: [solidRouterBrowserTracingIntegration()], + tunnel: 'http://localhost:3031/', // proxy server + // Performance Monitoring + tracesSampleRate: 1.0, // Capture 100% of the transactions + debug: !!import.meta.env.DEBUG, +}); + +mount(() => , document.getElementById('app')!); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx new file mode 100644 index 000000000000..276935366318 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { StartServer, createHandler } from '@solidjs/start/server'; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts new file mode 100644 index 000000000000..3dd5d8933b7b --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/instrument.server.ts @@ -0,0 +1,9 @@ +import * as Sentry from '@sentry/solidstart'; + +Sentry.init({ + dsn: process.env.E2E_TEST_DSN, + environment: 'qa', // dynamic sampling bias to keep transactions + tracesSampleRate: 1.0, // Capture 100% of the transactions + tunnel: 'http://localhost:3031/', // proxy server + debug: !!process.env.DEBUG, +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx new file mode 100644 index 000000000000..ddd970944bf3 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/back-navigation.tsx @@ -0,0 +1,9 @@ +import { A } from '@solidjs/router'; + +export default function BackNavigation() { + return ( + + User 6 + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx new file mode 100644 index 000000000000..5e405e8c4e40 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/client-error.tsx @@ -0,0 +1,15 @@ +export default function ClientErrorPage() { + return ( +
+ +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx new file mode 100644 index 000000000000..b22607667e7e --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/error-boundary.tsx @@ -0,0 +1,64 @@ +import * as Sentry from '@sentry/solidstart'; +import type { ParentProps } from 'solid-js'; +import { ErrorBoundary, createSignal, onMount } from 'solid-js'; + +const SentryErrorBoundary = Sentry.withSentryErrorBoundary(ErrorBoundary); + +const [count, setCount] = createSignal(1); +const [caughtError, setCaughtError] = createSignal(false); + +export default function ErrorBoundaryTestPage() { + return ( + + {caughtError() && ( + + )} +
+
+ +
+
+
+ ); +} + +function Throw(props: { error: string }) { + onMount(() => { + throw new Error(props.error); + }); + return null; +} + +function SampleErrorBoundary(props: ParentProps) { + return ( + ( +
+

Error Boundary Fallback

+
+ {error.message} +
+ +
+ )} + > + {props.children} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx new file mode 100644 index 000000000000..9a0b22cc38c6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/index.tsx @@ -0,0 +1,31 @@ +import { A } from '@solidjs/router'; + +export default function Home() { + return ( + <> +

Welcome to Solid Start

+

+ Visit docs.solidjs.com/solid-start to read the documentation +

+ + + ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx new file mode 100644 index 000000000000..05dce5e10a56 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/server-error.tsx @@ -0,0 +1,17 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + throw new Error('Error thrown from Solid Start E2E test app server route'); + + return { prefecture: 'Kanagawa' }; + }); +}; + +export default function ServerErrorPage() { + const data = createAsync(() => getPrefecture()); + + return
Prefecture: {data()?.prefecture}
; +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx new file mode 100644 index 000000000000..22abd3ba8803 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/src/routes/users/[id].tsx @@ -0,0 +1,21 @@ +import { withServerActionInstrumentation } from '@sentry/solidstart'; +import { createAsync, useParams } from '@solidjs/router'; + +const getPrefecture = async () => { + 'use server'; + return await withServerActionInstrumentation('getPrefecture', () => { + return { prefecture: 'Ehime' }; + }); +}; +export default function User() { + const params = useParams(); + const userData = createAsync(() => getPrefecture()); + + return ( +
+ User ID: {params.id} +
+ Prefecture: {userData()?.prefecture} +
+ ); +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs new file mode 100644 index 000000000000..46cc8824da18 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/start-event-proxy.mjs @@ -0,0 +1,6 @@ +import { startEventProxyServer } from '@sentry-internal/test-utils'; + +startEventProxyServer({ + port: 3031, + proxyServerName: 'solidstart-top-level-import', +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts new file mode 100644 index 000000000000..49f50f882b50 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errorboundary.test.ts @@ -0,0 +1,90 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test('captures an exception', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const errorEvent = await errorEventPromise; + + expect(errorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); + +test('captures a second exception after resetting the boundary', async ({ page }) => { + const firstErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.goto('/error-boundary'); + await page.locator('#caughtErrorBtn').click(); + const firstErrorEvent = await firstErrorEventPromise; + + expect(firstErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 1 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); + + const secondErrorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return ( + !errorEvent.type && + errorEvent.exception?.values?.[0]?.value === + 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app' + ); + }); + + await page.locator('#errorBoundaryResetBtn').click(); + await page.locator('#caughtErrorBtn').click(); + const secondErrorEvent = await secondErrorEventPromise; + + expect(secondErrorEvent).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error 2 thrown from Sentry ErrorBoundary in Solid Start E2E test app', + mechanism: { + type: 'generic', + handled: true, + }, + }, + ], + }, + transaction: '/error-boundary', + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts new file mode 100644 index 000000000000..9e4a0269eee4 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.client.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('client-side errors', () => { + test('captures error thrown on click', async ({ page }) => { + const errorPromise = waitForError('solidstart-top-level-import', async errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Uncaught error thrown from Solid Start E2E test app'; + }); + + await page.goto(`/client-error`); + await page.locator('#errorBtn').click(); + const error = await errorPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Uncaught error thrown from Solid Start E2E test app', + mechanism: { + handled: false, + }, + }, + ], + }, + transaction: '/client-error', + }); + expect(error.transaction).toEqual('/client-error'); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts new file mode 100644 index 000000000000..682dd34e10f9 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/errors.server.test.ts @@ -0,0 +1,30 @@ +import { expect, test } from '@playwright/test'; +import { waitForError } from '@sentry-internal/test-utils'; + +test.describe('server-side errors', () => { + test('captures server action error', async ({ page }) => { + const errorEventPromise = waitForError('solidstart-top-level-import', errorEvent => { + return errorEvent?.exception?.values?.[0]?.value === 'Error thrown from Solid Start E2E test app server route'; + }); + + await page.goto(`/server-error`); + + const error = await errorEventPromise; + + expect(error).toMatchObject({ + exception: { + values: [ + { + type: 'Error', + value: 'Error thrown from Solid Start E2E test app server route', + mechanism: { + type: 'solidstart', + handled: false, + }, + }, + ], + }, + // transaction: 'GET /server-error', --> only possible with `--import` CLI flag + }); + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts new file mode 100644 index 000000000000..bd5dece39b33 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.client.test.ts @@ -0,0 +1,95 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; + +test('sends a pageload transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/' && transactionEvent.contexts?.trace?.op === 'pageload'; + }); + + await page.goto('/'); + const pageloadTransaction = await transactionPromise; + + expect(pageloadTransaction).toMatchObject({ + contexts: { + trace: { + op: 'pageload', + origin: 'auto.pageload.browser', + }, + }, + transaction: '/', + transaction_info: { + source: 'url', + }, + }); +}); + +test('sends a navigation transaction', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/5' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/`); + await page.locator('#navLink').click(); + const navigationTransaction = await transactionPromise; + + expect(navigationTransaction).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/5', + transaction_info: { + source: 'url', + }, + }); +}); + +test('updates the transaction when using the back button', async ({ page }) => { + // Solid Router sends a `-1` navigation when using the back button. + // The sentry solidRouterBrowserTracingIntegration tries to update such + // transactions with the proper name once the `useLocation` hook triggers. + const navigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return transactionEvent?.transaction === '/users/6' && transactionEvent.contexts?.trace?.op === 'navigation'; + }); + + await page.goto(`/back-navigation`); + await page.locator('#navLink').click(); + const navigationTxn = await navigationTxnPromise; + + expect(navigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/users/6', + transaction_info: { + source: 'url', + }, + }); + + const backNavigationTxnPromise = waitForTransaction('solidstart-top-level-import', async transactionEvent => { + return ( + transactionEvent?.transaction === '/back-navigation' && transactionEvent.contexts?.trace?.op === 'navigation' + ); + }); + + await page.goBack(); + const backNavigationTxn = await backNavigationTxnPromise; + + expect(backNavigationTxn).toMatchObject({ + contexts: { + trace: { + op: 'navigation', + origin: 'auto.navigation.solidstart.solidrouter', + }, + }, + transaction: '/back-navigation', + transaction_info: { + source: 'url', + }, + }); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts new file mode 100644 index 000000000000..8072a7e75181 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tests/performance.server.test.ts @@ -0,0 +1,55 @@ +import { expect, test } from '@playwright/test'; +import { waitForTransaction } from '@sentry-internal/test-utils'; +import { + SEMANTIC_ATTRIBUTE_SENTRY_OP, + SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, + SEMANTIC_ATTRIBUTE_SENTRY_SOURCE, +} from '@sentry/core'; + +test('sends a server action transaction on pageload', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => { + return transactionEvent?.transaction === 'GET /users/6'; + }); + + await page.goto('/users/6'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); + +test('sends a server action transaction on client navigation', async ({ page }) => { + const transactionPromise = waitForTransaction('solidstart-top-level-import', transactionEvent => { + return transactionEvent?.transaction === 'POST getPrefecture'; + }); + + await page.goto('/'); + await page.locator('#navLink').click(); + await page.waitForURL('/users/5'); + + const transaction = await transactionPromise; + + expect(transaction.spans).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + description: 'getPrefecture', + data: { + [SEMANTIC_ATTRIBUTE_SENTRY_OP]: 'function.server_action', + [SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN]: 'auto.function.solidstart', + [SEMANTIC_ATTRIBUTE_SENTRY_SOURCE]: 'component', + }, + }), + ]), + ); +}); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json new file mode 100644 index 000000000000..6f11292cc5d8 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "node", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client", "vitest/globals", "@testing-library/jest-dom"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts new file mode 100644 index 000000000000..6c2b639dc300 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-import/vitest.config.ts @@ -0,0 +1,10 @@ +import solid from 'vite-plugin-solid'; +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + plugins: [solid()], + resolve: { + conditions: ['development', 'browser'], + }, + envPrefix: 'PUBLIC_', +}); diff --git a/packages/solidstart/.eslintrc.js b/packages/solidstart/.eslintrc.js index d567b12530d0..0fe78630b548 100644 --- a/packages/solidstart/.eslintrc.js +++ b/packages/solidstart/.eslintrc.js @@ -11,7 +11,7 @@ module.exports = { }, }, { - files: ['src/vite/**', 'src/server/**'], + files: ['src/vite/**', 'src/server/**', 'src/config/**'], rules: { '@sentry-internal/sdk/no-optional-chaining': 'off', '@sentry-internal/sdk/no-nullish-coalescing': 'off', diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts new file mode 100644 index 000000000000..74b72a12b4de --- /dev/null +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -0,0 +1,182 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { consoleSandbox } from '@sentry/core'; +import type { Nitro } from 'nitropack'; +import type { SentrySolidStartPluginOptions } from '../vite/types'; +import type { RollupConfig } from './types'; +import { wrapServerEntryWithDynamicImport } from './wrapServerEntryWithDynamicImport'; + +// Nitro presets for hosts that only host static files +export const staticHostPresets = ['github_pages']; +// Nitro presets for hosts that use `server.mjs` as opposed to `index.mjs` +export const serverFilePresets = ['netlify']; + +/** + * Adds the built `instrument.server.js` file to the output directory. + * + * As Sentry also imports the release injection file, this needs to be copied over manually as well. + * TODO: The mechanism of manually copying those files could maybe be improved + * + * This will no-op if no `instrument.server.js` file was found in the + * build directory. + */ +export async function addInstrumentationFileToBuild(nitro: Nitro): Promise { + nitro.hooks.hook('close', async () => { + // Static file hosts have no server component so there's nothing to do + if (staticHostPresets.includes(nitro.options.preset)) { + return; + } + + const buildDir = nitro.options.buildDir; + const serverDir = nitro.options.output.serverDir; + + try { + // 1. Create assets directory first (for release-injection-file) + const assetsServerDir = path.join(serverDir, 'assets'); + if (!fs.existsSync(assetsServerDir)) { + await fs.promises.mkdir(assetsServerDir, { recursive: true }); + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log(`[Sentry SolidStart withSentry] Successfully created directory ${assetsServerDir}.`); + }); + } + + // 2. Copy release injection file if available + try { + const ssrAssetsPath = path.resolve(buildDir, 'build', 'ssr', 'assets'); + const assetsBuildDir = await fs.promises.readdir(ssrAssetsPath); + const releaseInjectionFile = assetsBuildDir.find(file => file.startsWith('_sentry-release-injection-file-')); + + if (releaseInjectionFile) { + const releaseSource = path.resolve(ssrAssetsPath, releaseInjectionFile); + const releaseDestination = path.resolve(assetsServerDir, releaseInjectionFile); + + await fs.promises.copyFile(releaseSource, releaseDestination); + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log(`[Sentry SolidStart withSentry] Successfully created ${releaseDestination}.`); + }); + } + } catch (err) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('[Sentry SolidStart withSentry] Failed to copy release injection file.', err); + }); + } + + // 3. Copy Sentry server instrumentation file + const instrumentSource = path.resolve(buildDir, 'build', 'ssr', 'instrument.server.js'); + const instrumentDestination = path.resolve(serverDir, 'instrument.server.mjs'); + + await fs.promises.copyFile(instrumentSource, instrumentDestination); + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log(`[Sentry SolidStart withSentry] Successfully created ${instrumentDestination}.`); + }); + } catch (error) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn('[Sentry SolidStart withSentry] Failed to add instrumentation file to build.', error); + }); + } + }); +} + +/** + * Adds an `instrument.server.mjs` import to the top of the server entry file. + * + * This is meant as an escape hatch and should only be used in environments where + * it's not possible to `--import` the file instead as it comes with a limited + * tracing experience, only collecting http traces. + */ +export async function addSentryTopImport(nitro: Nitro): Promise { + nitro.hooks.hook('close', async () => { + const buildPreset = nitro.options.preset; + const serverDir = nitro.options.output.serverDir; + + // Static file hosts have no server component so there's nothing to do + if (staticHostPresets.includes(buildPreset)) { + return; + } + + const instrumentationFile = path.resolve(serverDir, 'instrument.server.mjs'); + const serverEntryFileName = serverFilePresets.includes(buildPreset) ? 'server.mjs' : 'index.mjs'; + const serverEntryFile = path.resolve(serverDir, serverEntryFileName); + + try { + await fs.promises.access(instrumentationFile, fs.constants.F_OK); + } catch (error) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry SolidStart withSentry] Failed to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`, + error, + ); + }); + return; + } + + try { + const content = await fs.promises.readFile(serverEntryFile, 'utf-8'); + const updatedContent = `import './instrument.server.mjs';\n${content}`; + await fs.promises.writeFile(serverEntryFile, updatedContent); + + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.log( + `[Sentry SolidStart withSentry] Added \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`, + ); + }); + } catch (error) { + // eslint-disable-next-line no-console + console.warn( + `[Sentry SolidStart withSentry] An error occurred when trying to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`, + error, + ); + } + }); +} + +/** + * This function modifies the Rollup configuration to include a plugin that wraps the entry file with a dynamic import (`import()`) + * and adds the Sentry server config with the static `import` declaration. + * + * With this, the Sentry server config can be loaded before all other modules of the application (which is needed for import-in-the-middle). + * See: https://nodejs.org/api/module.html#enabling + */ +export async function addDynamicImportEntryFileWrapper({ + nitro, + rollupConfig, + sentryPluginOptions, +}: { + nitro: Nitro; + rollupConfig: RollupConfig; + sentryPluginOptions: Omit & + Required>; +}): Promise { + // Static file hosts have no server component so there's nothing to do + if (staticHostPresets.includes(nitro.options.preset)) { + return; + } + + const srcDir = nitro.options.srcDir; + // todo allow other instrumentation paths + const serverInstrumentationPath = path.resolve(srcDir, 'src', 'instrument.server.ts'); + + const instrumentationFileName = sentryPluginOptions.instrumentation + ? path.basename(sentryPluginOptions.instrumentation) + : ''; + + rollupConfig.plugins.push( + wrapServerEntryWithDynamicImport({ + serverConfigFileName: sentryPluginOptions.instrumentation + ? path.join(path.dirname(instrumentationFileName), path.parse(instrumentationFileName).name) + : 'instrument.server', + serverEntrypointFileName: sentryPluginOptions.serverEntrypointFileName || nitro.options.preset, + resolvedServerConfigPath: serverInstrumentationPath, + entrypointWrappedFunctions: sentryPluginOptions.experimental_entrypointWrappedFunctions, + additionalImports: ['import-in-the-middle/hook.mjs'], + debug: sentryPluginOptions.debug, + }), + ); +} diff --git a/packages/solidstart/src/config/index.ts b/packages/solidstart/src/config/index.ts new file mode 100644 index 000000000000..4949f4bdf523 --- /dev/null +++ b/packages/solidstart/src/config/index.ts @@ -0,0 +1 @@ +export * from './withSentry'; diff --git a/packages/solidstart/src/config/types.ts b/packages/solidstart/src/config/types.ts new file mode 100644 index 000000000000..0d6ea9bdf4f4 --- /dev/null +++ b/packages/solidstart/src/config/types.ts @@ -0,0 +1,16 @@ +import type { defineConfig } from '@solidjs/start/config'; +import type { Nitro } from 'nitropack'; + +// Nitro does not export this type +export type RollupConfig = { + plugins: unknown[]; +}; + +export type SolidStartInlineConfig = Parameters[0]; + +export type SolidStartInlineServerConfig = { + hooks?: { + close?: () => unknown; + 'rollup:before'?: (nitro: Nitro) => unknown; + }; +}; diff --git a/packages/solidstart/src/config/utils.ts b/packages/solidstart/src/config/utils.ts new file mode 100644 index 000000000000..fd4b70d508d0 --- /dev/null +++ b/packages/solidstart/src/config/utils.ts @@ -0,0 +1,82 @@ +export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry'; +export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions='; +export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions='; +export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END'; + +/** + * Strips the Sentry query part from a path. + * Example: example/path?sentry-query-wrapped-entry?sentry-query-functions-reexport=foo,SENTRY-QUERY-END -> /example/path + * + * Only exported for testing. + */ +export function removeSentryQueryFromPath(url: string): string { + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const regex = new RegExp(`\\${SENTRY_WRAPPED_ENTRY}.*?\\${QUERY_END_INDICATOR}`); + return url.replace(regex, ''); +} + +/** + * Extracts and sanitizes function re-export and function wrap query parameters from a query string. + * If it is a default export, it is not considered for re-exporting. + * + * Only exported for testing. + */ +export function extractFunctionReexportQueryParameters(query: string): { wrap: string[]; reexport: string[] } { + // Regex matches the comma-separated params between the functions query + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const wrapRegex = new RegExp( + `\\${SENTRY_WRAPPED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR}|\\${SENTRY_REEXPORTED_FUNCTIONS})`, + ); + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const reexportRegex = new RegExp(`\\${SENTRY_REEXPORTED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR})`); + + const wrapMatch = query.match(wrapRegex); + const reexportMatch = query.match(reexportRegex); + + const wrap = + wrapMatch && wrapMatch[1] + ? wrapMatch[1] + .split(',') + .filter(param => param !== '') + // Sanitize, as code could be injected with another rollup plugin + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + const reexport = + reexportMatch && reexportMatch[1] + ? reexportMatch[1] + .split(',') + .filter(param => param !== '' && param !== 'default') + // Sanitize, as code could be injected with another rollup plugin + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + return { wrap, reexport }; +} + +/** + * Constructs a code snippet with function reexports (can be used in Rollup plugins as a return value for `load()`) + */ +export function constructFunctionReExport(pathWithQuery: string, entryId: string): string { + const { wrap: wrapFunctions, reexport: reexportFunctions } = extractFunctionReexportQueryParameters(pathWithQuery); + + return wrapFunctions + .reduce( + (functionsCode, currFunctionName) => + functionsCode.concat( + `async function ${currFunctionName}_sentryWrapped(...args) {\n` + + ` const res = await import(${JSON.stringify(entryId)});\n` + + ` return res.${currFunctionName}.call(this, ...args);\n` + + '}\n' + + `export { ${currFunctionName}_sentryWrapped as ${currFunctionName} };\n`, + ), + '', + ) + .concat( + reexportFunctions.reduce( + (functionsCode, currFunctionName) => + functionsCode.concat(`export { ${currFunctionName} } from ${JSON.stringify(entryId)};`), + '', + ), + ); +} diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts new file mode 100644 index 000000000000..c1050f0da1cc --- /dev/null +++ b/packages/solidstart/src/config/withSentry.ts @@ -0,0 +1,76 @@ +import { logger } from '@sentry/core'; +import type { Nitro } from 'nitropack'; +import { addSentryPluginToVite } from '../vite'; +import type { SentrySolidStartPluginOptions } from '../vite/types'; +import { + addDynamicImportEntryFileWrapper, + addInstrumentationFileToBuild, + addSentryTopImport, +} from './addInstrumentation'; +import type { RollupConfig, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types'; + +const defaultSentrySolidStartPluginOptions: Omit< + SentrySolidStartPluginOptions, + 'experimental_entrypointWrappedFunctions' +> & + Required> = { + experimental_entrypointWrappedFunctions: ['default', 'handler', 'server'], +}; + +/** + * Modifies the passed in Solid Start configuration with build-time enhancements such as + * building the `instrument.server.ts` file into the appropriate build folder based on + * build preset. + * + * @param solidStartConfig A Solid Start configuration object, as usually passed to `defineConfig` in `app.config.ts|js` + * @param sentrySolidStartPluginOptions Options to configure the plugin + * @returns The modified config to be exported and passed back into `defineConfig` + */ +export function withSentry( + solidStartConfig: SolidStartInlineConfig = {}, + sentrySolidStartPluginOptions: SentrySolidStartPluginOptions, +): SolidStartInlineConfig { + const sentryPluginOptions = { + ...sentrySolidStartPluginOptions, + ...defaultSentrySolidStartPluginOptions, + }; + + const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig; + const hooks = server.hooks || {}; + const vite = + typeof solidStartConfig.vite === 'function' + ? (...args: unknown[]) => addSentryPluginToVite(solidStartConfig.vite(...args), sentryPluginOptions) + : addSentryPluginToVite(solidStartConfig.vite, sentryPluginOptions); + + return { + ...solidStartConfig, + vite, + server: { + ...server, + hooks: { + ...hooks, + async 'rollup:before'(nitro: Nitro, config: RollupConfig) { + if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'experimental_dynamic-import') { + await addDynamicImportEntryFileWrapper({ nitro, rollupConfig: config, sentryPluginOptions }); + + sentrySolidStartPluginOptions.debug && + logger.log( + 'Wrapping the server entry file with a dynamic `import()`, so Sentry can be preloaded before the server initializes.', + ); + } else { + await addInstrumentationFileToBuild(nitro); + + if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'top-level-import') { + await addSentryTopImport(nitro); + } + } + + // Run user provided hook + if (hooks['rollup:before']) { + hooks['rollup:before'](nitro); + } + }, + }, + }, + }; +} diff --git a/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts new file mode 100644 index 000000000000..6d069220e1ae --- /dev/null +++ b/packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts @@ -0,0 +1,245 @@ +import { consoleSandbox } from '@sentry/core'; +import type { InputPluginOption } from 'rollup'; + +/** THIS FILE IS AN UTILITY FOR NITRO-BASED PACKAGES AND SHOULD BE KEPT IN SYNC IN NUXT, SOLIDSTART, ETC. */ + +export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry'; +export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions='; +export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions='; +export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END'; + +export type WrapServerEntryPluginOptions = { + serverEntrypointFileName: string; + serverConfigFileName: string; + resolvedServerConfigPath: string; + entrypointWrappedFunctions: string[]; + additionalImports?: string[]; + debug?: boolean; +}; + +/** + * A Rollup plugin which wraps the server entry with a dynamic `import()`. This makes it possible to initialize Sentry first + * by using a regular `import` and load the server after that. + * This also works with serverless `handler` functions, as it re-exports the `handler`. + * + * @param config Configuration options for the Rollup Plugin + * @param config.serverConfigFileName Name of the Sentry server config (without file extension). E.g. 'sentry.server.config' + * @param config.serverEntrypointFileName The server entrypoint (with file extension). Usually, this is defined by the Nitro preset and is something like 'node-server.mjs' + * @param config.resolvedServerConfigPath Resolved path of the Sentry server config (based on `src` directory) + * @param config.entryPointWrappedFunctions Exported bindings of the server entry file, which are wrapped as async function. E.g. ['default', 'handler', 'server'] + * @param config.additionalImports Adds additional imports to the entry file. Can be e.g. 'import-in-the-middle/hook.mjs' + * @param config.debug Whether debug logs are enabled in the build time environment + */ +export function wrapServerEntryWithDynamicImport(config: WrapServerEntryPluginOptions): InputPluginOption { + const { + serverConfigFileName, + serverEntrypointFileName, + resolvedServerConfigPath, + entrypointWrappedFunctions, + additionalImports, + debug, + } = config; + + // In order to correctly import the server config file + // and dynamically import the nitro runtime, we need to + // mark the resolutionId with '\0raw' to fall into the + // raw chunk group, c.f. https://github.com/nitrojs/nitro/commit/8b4a408231bdc222569a32ce109796a41eac4aa6#diff-e58102d2230f95ddeef2662957b48d847a6e891e354cfd0ae6e2e03ce848d1a2R142 + const resolutionIdPrefix = '\0raw'; + + return { + name: 'sentry-wrap-server-entry-with-dynamic-import', + async resolveId(source, importer, options) { + if (source.includes(`/${serverConfigFileName}`)) { + return { id: source, moduleSideEffects: true }; + } + + if (additionalImports && additionalImports.includes(source)) { + // When importing additional imports like "import-in-the-middle/hook.mjs" in the returned code of the `load()` function below: + // By setting `moduleSideEffects` to `true`, the import is added to the bundle, although nothing is imported from it + // By importing "import-in-the-middle/hook.mjs", we can make sure this file is included, as not all node builders are including files imported with `module.register()`. + // Prevents the error "Failed to register ESM hook Error: Cannot find module 'import-in-the-middle/hook.mjs'" + return { id: source, moduleSideEffects: true, external: true }; + } + + if ( + options.isEntry && + source.includes(serverEntrypointFileName) && + source.includes('.mjs') && + !source.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) + ) { + const resolution = await this.resolve(source, importer, options); + + // If it cannot be resolved or is external, just return it so that Rollup can display an error + if (!resolution || (resolution && resolution.external)) return resolution; + + const moduleInfo = await this.load(resolution); + + moduleInfo.moduleSideEffects = true; + + // The enclosing `if` already checks for the suffix in `source`, but a check in `resolution.id` is needed as well to prevent multiple attachment of the suffix + return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`) + ? resolution.id + : `${resolutionIdPrefix}${resolution.id + // Concatenates the query params to mark the file (also attaches names of re-exports - this is needed for serverless functions to re-export the handler) + .concat(SENTRY_WRAPPED_ENTRY) + .concat( + constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug), + ) + .concat(QUERY_END_INDICATOR)}`; + } + return null; + }, + load(id: string) { + if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { + const entryId = removeSentryQueryFromPath(id).slice(resolutionIdPrefix.length); + + // Mostly useful for serverless `handler` functions + const reExportedFunctions = + id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS) + ? constructFunctionReExport(id, entryId) + : ''; + + return ( + // Regular `import` of the Sentry config + `import ${JSON.stringify(resolvedServerConfigPath)};\n` + + // Dynamic `import()` for the previous, actual entry point. + // `import()` can be used for any code that should be run after the hooks are registered (https://nodejs.org/api/module.html#enabling) + `import(${JSON.stringify(entryId)});\n` + + // By importing additional imports like "import-in-the-middle/hook.mjs", we can make sure this file wil be included, as not all node builders are including files imported with `module.register()`. + `${additionalImports ? additionalImports.map(importPath => `import "${importPath}";\n`) : ''}` + + `${reExportedFunctions}\n` + ); + } + + return null; + }, + }; +} + +/** + * Strips the Sentry query part from a path. + * Example: example/path?sentry-query-wrapped-entry?sentry-query-functions-reexport=foo,SENTRY-QUERY-END -> /example/path + * + * **Only exported for testing** + */ +export function removeSentryQueryFromPath(url: string): string { + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const regex = new RegExp(`\\${SENTRY_WRAPPED_ENTRY}.*?\\${QUERY_END_INDICATOR}`); + return url.replace(regex, ''); +} + +/** + * Extracts and sanitizes function re-export and function wrap query parameters from a query string. + * If it is a default export, it is not considered for re-exporting. + * + * **Only exported for testing** + */ +export function extractFunctionReexportQueryParameters(query: string): { wrap: string[]; reexport: string[] } { + // Regex matches the comma-separated params between the functions query + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const wrapRegex = new RegExp( + `\\${SENTRY_WRAPPED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR}|\\${SENTRY_REEXPORTED_FUNCTIONS})`, + ); + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const reexportRegex = new RegExp(`\\${SENTRY_REEXPORTED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR})`); + + const wrapMatch = query.match(wrapRegex); + const reexportMatch = query.match(reexportRegex); + + const wrap = + wrapMatch && wrapMatch[1] + ? wrapMatch[1] + .split(',') + .filter(param => param !== '') + // Sanitize, as code could be injected with another rollup plugin + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + const reexport = + reexportMatch && reexportMatch[1] + ? reexportMatch[1] + .split(',') + .filter(param => param !== '' && param !== 'default') + // Sanitize, as code could be injected with another rollup plugin + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; + + return { wrap, reexport }; +} + +/** + * Constructs a comma-separated string with all functions that need to be re-exported later from the server entry. + * It uses Rollup's `exportedBindings` to determine the functions to re-export. Functions which should be wrapped + * (e.g. serverless handlers) are wrapped by Sentry. + * + * **Only exported for testing** + */ +export function constructWrappedFunctionExportQuery( + exportedBindings: Record | null, + entrypointWrappedFunctions: string[], + debug?: boolean, +): string { + const functionsToExport: { wrap: string[]; reexport: string[] } = { + wrap: [], + reexport: [], + }; + + // `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }` + // The key `.` refers to exports within the current file, while other keys show from where exports were imported first. + Object.values(exportedBindings || {}).forEach(functions => + functions.forEach(fn => { + if (entrypointWrappedFunctions.includes(fn)) { + functionsToExport.wrap.push(fn); + } else { + functionsToExport.reexport.push(fn); + } + }), + ); + + if (debug && functionsToExport.wrap.length === 0) { + consoleSandbox(() => + // eslint-disable-next-line no-console + console.warn( + '[Sentry] No functions found to wrap. In case the server needs to export async functions other than `handler` or `server`, consider adding the name(s) to `entrypointWrappedFunctions`.', + ), + ); + } + + const wrapQuery = functionsToExport.wrap.length + ? `${SENTRY_WRAPPED_FUNCTIONS}${functionsToExport.wrap.join(',')}` + : ''; + const reexportQuery = functionsToExport.reexport.length + ? `${SENTRY_REEXPORTED_FUNCTIONS}${functionsToExport.reexport.join(',')}` + : ''; + + return [wrapQuery, reexportQuery].join(''); +} + +/** + * Constructs a code snippet with function reexports (can be used in Rollup plugins as a return value for `load()`) + * + * **Only exported for testing** + */ +export function constructFunctionReExport(pathWithQuery: string, entryId: string): string { + const { wrap: wrapFunctions, reexport: reexportFunctions } = extractFunctionReexportQueryParameters(pathWithQuery); + + return wrapFunctions + .reduce( + (functionsCode, currFunctionName) => + functionsCode.concat( + `async function ${currFunctionName}_sentryWrapped(...args) {\n` + + ` const res = await import(${JSON.stringify(entryId)});\n` + + ` return res.${currFunctionName}.call(this, ...args);\n` + + '}\n' + + `export { ${currFunctionName}_sentryWrapped as ${currFunctionName} };\n`, + ), + '', + ) + .concat( + reexportFunctions.reduce( + (functionsCode, currFunctionName) => + functionsCode.concat(`export { ${currFunctionName} } from ${JSON.stringify(entryId)};`), + '', + ), + ); +} diff --git a/packages/solidstart/src/index.server.ts b/packages/solidstart/src/index.server.ts index d675a1c72820..a20a0367f557 100644 --- a/packages/solidstart/src/index.server.ts +++ b/packages/solidstart/src/index.server.ts @@ -1,2 +1,3 @@ export * from './server'; export * from './vite'; +export * from './config'; diff --git a/packages/solidstart/src/index.types.ts b/packages/solidstart/src/index.types.ts index 434f13959f44..5b43674d959a 100644 --- a/packages/solidstart/src/index.types.ts +++ b/packages/solidstart/src/index.types.ts @@ -4,6 +4,7 @@ export * from './client'; export * from './server'; export * from './vite'; +export * from './config'; import type { Client, Integration, Options, StackParser } from '@sentry/core'; diff --git a/packages/solidstart/src/vite/buildInstrumentationFile.ts b/packages/solidstart/src/vite/buildInstrumentationFile.ts new file mode 100644 index 000000000000..81bcef7a5bf7 --- /dev/null +++ b/packages/solidstart/src/vite/buildInstrumentationFile.ts @@ -0,0 +1,55 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { consoleSandbox } from '@sentry/core'; +import type { Plugin, UserConfig } from 'vite'; +import type { SentrySolidStartPluginOptions } from './types'; + +/** + * A Sentry plugin for SolidStart to build the server + * `instrument.server.ts` file. + */ +export function makeBuildInstrumentationFilePlugin(options: SentrySolidStartPluginOptions = {}): Plugin { + return { + name: 'sentry-solidstart-build-instrumentation-file', + apply: 'build', + enforce: 'post', + async config(config: UserConfig, { command }) { + const instrumentationFilePath = options.instrumentation || './src/instrument.server.ts'; + const router = (config as UserConfig & { router: { target: string; name: string; root: string } }).router; + const build = config.build || {}; + const rollupOptions = build.rollupOptions || {}; + const input = [...((rollupOptions.input || []) as string[])]; + + // plugin runs for client, server and sever-fns, we only want to run it for the server once. + if (command !== 'build' || router.target !== 'server' || router.name === 'server-fns') { + return config; + } + + try { + await fs.promises.access(instrumentationFilePath, fs.constants.F_OK); + } catch (error) { + consoleSandbox(() => { + // eslint-disable-next-line no-console + console.warn( + `[Sentry SolidStart Plugin] Could not access \`${instrumentationFilePath}\`, please make sure it exists.`, + error, + ); + }); + return config; + } + + input.push(path.resolve(router.root, instrumentationFilePath)); + + return { + ...config, + build: { + ...build, + rollupOptions: { + ...rollupOptions, + input, + }, + }, + }; + }, + }; +} diff --git a/packages/solidstart/src/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts index 59435f919071..da0a3e116a0a 100644 --- a/packages/solidstart/src/vite/sentrySolidStartVite.ts +++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts @@ -1,13 +1,36 @@ -import type { Plugin } from 'vite'; +import type { Plugin, UserConfig } from 'vite'; +import { makeBuildInstrumentationFilePlugin } from './buildInstrumentationFile'; import { makeSourceMapsVitePlugin } from './sourceMaps'; import type { SentrySolidStartPluginOptions } from './types'; +// todo(v9): Don't export to users anymore and remove deprecation (and eslint warning silencing) when it's not exported anymore /** * Various Sentry vite plugins to be used for SolidStart. + * + * @deprecated This plugin will be removed in v9. Instead, use `withSentry` to wrap your SolidStart config. Example: + * ``` + * export default defineConfig( + * withSentry( + * { + * // SolidStart config... + * }, + * { + * // Sentry config + * org: process.env.SENTRY_ORG, + * project: process.env.SENTRY_PROJECT, + * authToken: process.env.SENTRY_AUTH_TOKEN, + * }, + * ), + * ); + * ``` */ export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}): Plugin[] => { const sentryPlugins: Plugin[] = []; + if (options.autoInjectServerSentry !== 'experimental_dynamic-import') { + sentryPlugins.push(makeBuildInstrumentationFilePlugin(options)); + } + if (process.env.NODE_ENV !== 'development') { if (options.sourceMapsUploadOptions?.enabled ?? true) { sentryPlugins.push(...makeSourceMapsVitePlugin(options)); @@ -16,3 +39,17 @@ export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {} return sentryPlugins; }; + +/** + * Helper to add the Sentry SolidStart vite plugin to a vite config. + */ +export const addSentryPluginToVite = (config: UserConfig = {}, options: SentrySolidStartPluginOptions): UserConfig => { + const plugins = Array.isArray(config.plugins) ? [...config.plugins] : []; + // eslint-disable-next-line deprecation/deprecation + plugins.unshift(sentrySolidStartVite(options)); + + return { + ...config, + plugins, + }; +}; diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index 4a64e4856b5d..1ae73777c6a4 100644 --- a/packages/solidstart/src/vite/types.ts +++ b/packages/solidstart/src/vite/types.ts @@ -85,7 +85,7 @@ type BundleSizeOptimizationOptions = { }; /** - * Build options for the Sentry module. These options are used during build-time by the Sentry SDK. + * Build options for the Sentry plugin. These options are used during build-time by the Sentry SDK. */ export type SentrySolidStartPluginOptions = { /** @@ -125,4 +125,59 @@ export type SentrySolidStartPluginOptions = { * Enabling this will give you, for example logs about source maps. */ debug?: boolean; + + /** + * The path to your `instrument.server.ts|js` file. + * e.g. `./src/instrument.server.ts` + * + * Defaults to: `./src/instrument.server.ts` + */ + instrumentation?: string; + + /** + * The server entrypoint filename is automatically set by the Sentry SDK depending on the Nitro present. + * In case the server entrypoint has a different filename, you can overwrite it here. + */ + serverEntrypointFileName?: string; + + /** + * + * Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible. + * + * **DO NOT** add the node CLI flag `--import` in your node start script, when auto-injecting Sentry. + * This would initialize Sentry twice on the server-side and this leads to unexpected issues. + * + * --- + * + * **"top-level-import"** + * + * Enabling basic server tracing with top-level import can be used for environments where modifying the node option `--import` is not possible. + * However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.). + * + * If `"top-level-import"` is enabled, the Sentry SDK will import the Sentry server config at the top of the server entry file to load the SDK on the server. + * + * --- + * **"experimental_dynamic-import"** + * + * Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register + * necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling) + * + * If `"experimental_dynamic-import"` is enabled, the Sentry SDK wraps the server entry file with `import()`. + * + * @default undefined + */ + autoInjectServerSentry?: 'top-level-import' | 'experimental_dynamic-import'; + + /** + * When `autoInjectServerSentry` is set to `"experimental_dynamic-import"`, the SDK will wrap your Nitro server entrypoint + * with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported. + * Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is. + * + * By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint. + * If your server has a different main export that is used to run the server, you can overwrite this by providing an array of export names to wrap. + * Any wrapped export is expected to be an async function. + * + * @default ['default', 'handler', 'server'] + */ + experimental_entrypointWrappedFunctions?: string[]; }; diff --git a/packages/solidstart/test/config/addInstrumentation.test.ts b/packages/solidstart/test/config/addInstrumentation.test.ts new file mode 100644 index 000000000000..012bca76c9ca --- /dev/null +++ b/packages/solidstart/test/config/addInstrumentation.test.ts @@ -0,0 +1,222 @@ +import type { Nitro } from 'nitropack'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { + addDynamicImportEntryFileWrapper, + addInstrumentationFileToBuild, + staticHostPresets, +} from '../../src/config/addInstrumentation'; +import type { RollupConfig } from '../../src/config/types'; + +const consoleLogSpy = vi.spyOn(console, 'log'); +const consoleWarnSpy = vi.spyOn(console, 'warn'); +const fsAccessMock = vi.fn(); +const fsCopyFileMock = vi.fn(); +const fsReadFile = vi.fn(); +const fsWriteFileMock = vi.fn(); +const fsMkdirMock = vi.fn(); +const fsReaddirMock = vi.fn(); +const fsExistsSyncMock = vi.fn(); + +vi.mock('fs', async () => { + const actual = await vi.importActual('fs'); + return { + ...actual, + existsSync: (...args: unknown[]) => fsExistsSyncMock(...args), + promises: { + // @ts-expect-error this exists + ...actual.promises, + access: (...args: unknown[]) => fsAccessMock(...args), + copyFile: (...args: unknown[]) => fsCopyFileMock(...args), + readFile: (...args: unknown[]) => fsReadFile(...args), + writeFile: (...args: unknown[]) => fsWriteFileMock(...args), + mkdir: (...args: unknown[]) => fsMkdirMock(...args), + readdir: (...args: unknown[]) => fsReaddirMock(...args), + }, + }; +}); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('addInstrumentationFileToBuild()', () => { + const nitroOptions: Nitro = { + hooks: { + hook: vi.fn(), + }, + options: { + buildDir: '/path/to/buildDir', + output: { + serverDir: '/path/to/serverDir', + }, + preset: 'vercel', + }, + }; + + const callNitroCloseHook = async () => { + const hookCallback = nitroOptions.hooks.hook.mock.calls[0][1]; + await hookCallback(); + }; + + it('adds `instrument.server.mjs` to the server output directory', async () => { + fsCopyFileMock.mockResolvedValueOnce(true); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsCopyFileMock).toHaveBeenCalledWith( + '/path/to/buildDir/build/ssr/instrument.server.js', + '/path/to/serverDir/instrument.server.mjs', + ); + }); + + it('warns when `instrument.server.js` cannot be copied to the server output directory', async () => { + const error = new Error('Failed to copy file.'); + fsCopyFileMock.mockRejectedValueOnce(error); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsCopyFileMock).toHaveBeenCalledWith( + '/path/to/buildDir/build/ssr/instrument.server.js', + '/path/to/serverDir/instrument.server.mjs', + ); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[Sentry SolidStart withSentry] Failed to add instrumentation file to build.', + error, + ); + }); + + it.each(staticHostPresets)("doesn't add `instrument.server.mjs` for static host `%s`", async preset => { + const staticNitroOptions = { + ...nitroOptions, + options: { + ...nitroOptions.options, + preset, + }, + }; + + await addInstrumentationFileToBuild(staticNitroOptions); + + await callNitroCloseHook(); + + expect(fsCopyFileMock).not.toHaveBeenCalled(); + }); + + it('creates assets directory if it does not exist', async () => { + fsExistsSyncMock.mockReturnValue(false); + fsMkdirMock.mockResolvedValueOnce(true); + fsCopyFileMock.mockResolvedValueOnce(true); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsMkdirMock).toHaveBeenCalledWith('/path/to/serverDir/assets', { recursive: true }); + expect(consoleLogSpy).toHaveBeenCalledWith( + '[Sentry SolidStart withSentry] Successfully created directory /path/to/serverDir/assets.', + ); + }); + + it('does not create assets directory if it already exists', async () => { + fsExistsSyncMock.mockReturnValue(true); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsMkdirMock).not.toHaveBeenCalled(); + }); + + it('copies release injection file if available', async () => { + fsExistsSyncMock.mockReturnValue(true); + fsReaddirMock.mockResolvedValueOnce(['_sentry-release-injection-file-test.js']); + fsCopyFileMock.mockResolvedValueOnce(true); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsCopyFileMock).toHaveBeenCalledWith( + '/path/to/buildDir/build/ssr/assets/_sentry-release-injection-file-test.js', + '/path/to/serverDir/assets/_sentry-release-injection-file-test.js', + ); + expect(consoleLogSpy).toHaveBeenCalledWith( + '[Sentry SolidStart withSentry] Successfully created /path/to/serverDir/assets/_sentry-release-injection-file-test.js.', + ); + }); + + it('warns when release injection file cannot be copied', async () => { + const error = new Error('Failed to copy release injection file.'); + fsExistsSyncMock.mockReturnValue(true); + fsReaddirMock.mockResolvedValueOnce(['_sentry-release-injection-file-test.js']); + fsCopyFileMock.mockRejectedValueOnce(error); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsCopyFileMock).toHaveBeenCalledWith( + '/path/to/buildDir/build/ssr/assets/_sentry-release-injection-file-test.js', + '/path/to/serverDir/assets/_sentry-release-injection-file-test.js', + ); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[Sentry SolidStart withSentry] Failed to copy release injection file.', + error, + ); + }); + + it('does not copy release injection file if not found', async () => { + fsExistsSyncMock.mockReturnValue(true); + fsReaddirMock.mockResolvedValueOnce([]); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsCopyFileMock).not.toHaveBeenCalledWith( + expect.stringContaining('_sentry-release-injection-file-'), + expect.any(String), + ); + }); + + it('warns when `instrument.server.js` is not found', async () => { + const error = new Error('File not found'); + fsCopyFileMock.mockRejectedValueOnce(error); + await addInstrumentationFileToBuild(nitroOptions); + + await callNitroCloseHook(); + + expect(fsCopyFileMock).toHaveBeenCalledWith( + '/path/to/buildDir/build/ssr/instrument.server.js', + '/path/to/serverDir/instrument.server.mjs', + ); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[Sentry SolidStart withSentry] Failed to add instrumentation file to build.', + error, + ); + }); +}); + +describe('addAutoInstrumentation()', () => { + const nitroOptions: Nitro = { + options: { + srcDir: 'path/to/srcDir', + buildDir: '/path/to/buildDir', + output: { + serverDir: '/path/to/serverDir', + }, + preset: 'vercel', + }, + }; + + it('adds the `sentry-wrap-server-entry-with-dynamic-import` rollup plugin to the rollup config', async () => { + const rollupConfig: RollupConfig = { + plugins: [], + }; + + await addDynamicImportEntryFileWrapper({ + nitro: nitroOptions, + rollupConfig, + sentryPluginOptions: { experimental_entrypointWrappedFunctions: [] }, + }); + expect( + rollupConfig.plugins.find(plugin => plugin.name === 'sentry-wrap-server-entry-with-dynamic-import'), + ).toBeTruthy(); + }); +}); diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts new file mode 100644 index 000000000000..e554db45124f --- /dev/null +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -0,0 +1,152 @@ +import type { Nitro } from 'nitropack'; +import type { Plugin } from 'vite'; +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { withSentry } from '../../src/config'; + +const userDefinedNitroRollupBeforeHookMock = vi.fn(); +const userDefinedNitroCloseHookMock = vi.fn(); +const addInstrumentationFileToBuildMock = vi.fn(); +const addSentryTopImportMock = vi.fn(); + +vi.mock('../../src/config/addInstrumentation', () => ({ + addInstrumentationFileToBuild: (...args: unknown[]) => addInstrumentationFileToBuildMock(...args), + addSentryTopImport: (...args: unknown[]) => addSentryTopImportMock(...args), +})); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('withSentry()', () => { + const solidStartConfig = { + middleware: './src/middleware.ts', + server: { + hooks: { + close: userDefinedNitroCloseHookMock, + 'rollup:before': userDefinedNitroRollupBeforeHookMock, + }, + }, + }; + const nitroOptions: Nitro = { + options: { + buildDir: '/path/to/buildDir', + output: { + serverDir: '/path/to/serverDir', + }, + preset: 'vercel', + }, + }; + + it('adds a nitro hook to add the instrumentation file to the build if no plugin options are provided', async () => { + const config = withSentry(solidStartConfig, {}); + await config?.server.hooks['rollup:before'](nitroOptions); + expect(addInstrumentationFileToBuildMock).toHaveBeenCalledWith(nitroOptions); + expect(userDefinedNitroRollupBeforeHookMock).toHaveBeenCalledWith(nitroOptions); + }); + + it('adds a nitro hook to add the instrumentation file as top level import to the server entry file when configured in autoInjectServerSentry', async () => { + const config = withSentry(solidStartConfig, { autoInjectServerSentry: 'top-level-import' }); + await config?.server.hooks['rollup:before'](nitroOptions); + await config?.server.hooks['close'](nitroOptions); + expect(addSentryTopImportMock).toHaveBeenCalledWith( + expect.objectContaining({ + options: { + buildDir: '/path/to/buildDir', + output: { + serverDir: '/path/to/serverDir', + }, + preset: 'vercel', + }, + }), + ); + expect(userDefinedNitroCloseHookMock).toHaveBeenCalled(); + }); + + it('does not add the instrumentation file as top level import if autoInjectServerSentry is undefined', async () => { + const config = withSentry(solidStartConfig, { autoInjectServerSentry: undefined }); + await config?.server.hooks['rollup:before'](nitroOptions); + await config?.server.hooks['close'](nitroOptions); + expect(addSentryTopImportMock).not.toHaveBeenCalled(); + expect(userDefinedNitroCloseHookMock).toHaveBeenCalled(); + }); + + it('adds the sentry solidstart vite plugin', () => { + const config = withSentry(solidStartConfig, { + project: 'project', + org: 'org', + authToken: 'token', + }); + const names = config?.vite.plugins.flat().map((plugin: Plugin) => plugin.name); + expect(names).toEqual([ + 'sentry-solidstart-build-instrumentation-file', + 'sentry-solidstart-source-maps', + 'sentry-telemetry-plugin', + 'sentry-vite-release-injection-plugin', + 'sentry-debug-id-upload-plugin', + 'sentry-vite-debug-id-injection-plugin', + 'sentry-vite-debug-id-upload-plugin', + 'sentry-file-deletion-plugin', + ]); + }); + + it('extends the passed in vite config object', () => { + const config = withSentry( + { + ...solidStartConfig, + vite: { + plugins: [{ name: 'my-test-plugin' }], + }, + }, + { + project: 'project', + org: 'org', + authToken: 'token', + }, + ); + + const names = config?.vite.plugins.flat().map((plugin: Plugin) => plugin.name); + expect(names).toEqual([ + 'sentry-solidstart-build-instrumentation-file', + 'sentry-solidstart-source-maps', + 'sentry-telemetry-plugin', + 'sentry-vite-release-injection-plugin', + 'sentry-debug-id-upload-plugin', + 'sentry-vite-debug-id-injection-plugin', + 'sentry-vite-debug-id-upload-plugin', + 'sentry-file-deletion-plugin', + 'my-test-plugin', + ]); + }); + + it('extends the passed in vite function config', () => { + const config = withSentry( + { + ...solidStartConfig, + vite() { + return { plugins: [{ name: 'my-test-plugin' }] }; + }, + }, + { + project: 'project', + org: 'org', + authToken: 'token', + }, + ); + + const names = config + ?.vite() + .plugins.flat() + .map((plugin: Plugin) => plugin.name); + expect(names).toEqual([ + 'sentry-solidstart-build-instrumentation-file', + 'sentry-solidstart-source-maps', + 'sentry-telemetry-plugin', + 'sentry-vite-release-injection-plugin', + 'sentry-debug-id-upload-plugin', + 'sentry-vite-debug-id-injection-plugin', + 'sentry-vite-debug-id-upload-plugin', + 'sentry-file-deletion-plugin', + 'my-test-plugin', + ]); + }); +}); diff --git a/packages/solidstart/test/vite/buildInstrumentation.test.ts b/packages/solidstart/test/vite/buildInstrumentation.test.ts new file mode 100644 index 000000000000..52378a668870 --- /dev/null +++ b/packages/solidstart/test/vite/buildInstrumentation.test.ts @@ -0,0 +1,130 @@ +import type { UserConfig } from 'vite'; +import { describe, expect, it, vi } from 'vitest'; +import { makeBuildInstrumentationFilePlugin } from '../../src/vite/buildInstrumentationFile'; + +const fsAccessMock = vi.fn(); + +vi.mock('fs', async () => { + const actual = await vi.importActual('fs'); + return { + ...actual, + promises: { + // @ts-expect-error this exists + ...actual.promises, + access: () => fsAccessMock(), + }, + }; +}); + +const consoleWarnSpy = vi.spyOn(console, 'warn'); + +beforeEach(() => { + vi.clearAllMocks(); +}); + +describe('makeBuildInstrumentationFilePlugin()', () => { + const viteConfig: UserConfig & { router: { target: string; name: string; root: string } } = { + router: { + target: 'server', + name: 'ssr', + root: '/some/project/path', + }, + build: { + rollupOptions: { + input: ['/path/to/entry1.js', '/path/to/entry2.js'], + }, + }, + }; + + it('returns a plugin to set `sourcemaps` to `true`', () => { + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin(); + + expect(buildInstrumentationFilePlugin.name).toEqual('sentry-solidstart-build-instrumentation-file'); + expect(buildInstrumentationFilePlugin.apply).toEqual('build'); + expect(buildInstrumentationFilePlugin.enforce).toEqual('post'); + expect(buildInstrumentationFilePlugin.config).toEqual(expect.any(Function)); + }); + + it('adds the instrumentation file for server builds', async () => { + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - this is always defined and always a function + const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' }); + expect(config.build.rollupOptions.input).toContain('/some/project/path/src/instrument.server.ts'); + }); + + it('adds the correct instrumentation file', async () => { + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin({ + instrumentation: './src/myapp/instrument.server.ts', + }); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - this is always defined and always a function + const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' }); + expect(config.build.rollupOptions.input).toContain('/some/project/path/src/myapp/instrument.server.ts'); + }); + + it("doesn't add the instrumentation file for server function builds", async () => { + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - this is always defined and always a function + const config = await buildInstrumentationFilePlugin.config( + { + ...viteConfig, + router: { + ...viteConfig.router, + name: 'server-fns', + }, + }, + { command: 'build' }, + ); + expect(config.build.rollupOptions.input).not.toContain('/some/project/path/src/instrument.server.ts'); + }); + + it("doesn't add the instrumentation file for client builds", async () => { + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - this is always defined and always a function + const config = await buildInstrumentationFilePlugin.config( + { + ...viteConfig, + router: { + ...viteConfig.router, + target: 'client', + }, + }, + { command: 'build' }, + ); + expect(config.build.rollupOptions.input).not.toContain('/some/project/path/src/instrument.server.ts'); + }); + + it("doesn't add the instrumentation file when serving", async () => { + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - this is always defined and always a function + const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'serve' }); + expect(config.build.rollupOptions.input).not.toContain('/some/project/path/src/instrument.server.ts'); + }); + + it("doesn't modify the config if the instrumentation file doesn't exist", async () => { + fsAccessMock.mockRejectedValueOnce(undefined); + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - this is always defined and always a function + const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' }); + expect(config).toEqual(viteConfig); + }); + + it("logs a warning if the instrumentation file doesn't exist", async () => { + const error = new Error("File doesn't exist."); + fsAccessMock.mockRejectedValueOnce(error); + const buildInstrumentationFilePlugin = makeBuildInstrumentationFilePlugin(); + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore - this is always defined and always a function + const config = await buildInstrumentationFilePlugin.config(viteConfig, { command: 'build' }); + expect(config).toEqual(viteConfig); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[Sentry SolidStart Plugin] Could not access `./src/instrument.server.ts`, please make sure it exists.', + error, + ); + }); +}); diff --git a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts index d3f905313859..880d7dff4f69 100644 --- a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts +++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts @@ -9,7 +9,9 @@ vi.spyOn(console, 'warn').mockImplementation(() => { /* noop */ }); +// eslint-disable-next-line deprecation/deprecation function getSentrySolidStartVitePlugins(options?: Parameters[0]): Plugin[] { + // eslint-disable-next-line deprecation/deprecation return sentrySolidStartVite({ project: 'project', org: 'org', @@ -23,6 +25,7 @@ describe('sentrySolidStartVite()', () => { const plugins = getSentrySolidStartVitePlugins(); const names = plugins.map(plugin => plugin.name); expect(names).toEqual([ + 'sentry-solidstart-build-instrumentation-file', 'sentry-solidstart-source-maps', 'sentry-telemetry-plugin', 'sentry-vite-release-injection-plugin', @@ -33,17 +36,19 @@ describe('sentrySolidStartVite()', () => { ]); }); - it("returns an empty array if source maps upload isn't enabled", () => { + it("returns only build-instrumentation-file plugin if source maps upload isn't enabled", () => { const plugins = getSentrySolidStartVitePlugins({ sourceMapsUploadOptions: { enabled: false } }); - expect(plugins).toHaveLength(0); + const names = plugins.map(plugin => plugin.name); + expect(names).toEqual(['sentry-solidstart-build-instrumentation-file']); }); - it('returns an empty array if `NODE_ENV` is development', async () => { + it('returns only build-instrumentation-file plugin if `NODE_ENV` is development', async () => { const previousEnv = process.env.NODE_ENV; process.env.NODE_ENV = 'development'; const plugins = getSentrySolidStartVitePlugins({ sourceMapsUploadOptions: { enabled: true } }); - expect(plugins).toHaveLength(0); + const names = plugins.map(plugin => plugin.name); + expect(names).toEqual(['sentry-solidstart-build-instrumentation-file']); process.env.NODE_ENV = previousEnv; });