diff --git a/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts
index 0b9a5553fb0a..337068467558 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts
@@ -1,8 +1,23 @@
-import { sentrySolidStartVite } from '@sentry/solidstart';
+import { withSentry } from '@sentry/solidstart';
import { defineConfig } from '@solidjs/start/config';
-export default defineConfig({
- vite: {
- plugins: [sentrySolidStartVite()],
- },
-});
+export default defineConfig(
+ withSentry(
+ {},
+ {
+ // Typically we want to default to ./src/instrument.sever.ts
+ // `withSentry` would then build and copy the file over to
+ // the .output folder, but since we can't use the production
+ // server for our e2e tests, we have to delete the build folders
+ // prior to using the dev server for our tests. Which also gets
+ // rid of the instrument.server.mjs file that we need to --import.
+ // Therefore, we specify the .mjs file here and to ensure
+ // `withSentry` gets its file to build and we continue to reference
+ // the file from the `src` folder for --import without needing to
+ // transpile before.
+ // This can be removed once we get the production server to work
+ // with our e2e tests.
+ instrumentation: './src/instrument.server.mjs',
+ },
+ ),
+);
diff --git a/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts b/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts
index b709760aab94..088f69df6380 100644
--- a/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts
+++ b/dev-packages/e2e-tests/test-applications/solidstart/tests/errorboundary.test.ts
@@ -11,6 +11,8 @@ test('captures an exception', async ({ page }) => {
});
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;
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/README.md b/packages/solidstart/README.md
index ceda55838e8d..2ec876b35c8c 100644
--- a/packages/solidstart/README.md
+++ b/packages/solidstart/README.md
@@ -60,7 +60,7 @@ mount(() => , document.getElementById('app'));
### 3. Server-side Setup
-Create an instrument file named `instrument.server.mjs` and add your initialization code for the server-side SDK.
+Create an instrument file named `src/instrument.server.ts` and add your initialization code for the server-side SDK.
```javascript
import * as Sentry from '@sentry/solidstart';
@@ -101,16 +101,94 @@ export default defineConfig({
The Sentry middleware enhances the data collected by Sentry on the server side by enabling distributed tracing between
the client and server.
-### 5. Run your application
+### 5. Configure your application
+
+For Sentry to work properly, SolidStart's `app.config.ts` has to be modified. Wrap your config with `withSentry` and
+configure it to upload source maps.
+
+If your `instrument.server.ts` file is not located in the `src` folder, you can specify the path via the
+`instrumentation` option to `withSentry`.
+
+To upload source maps, configure an auth token. Auth tokens can be passed explicitly with the `authToken` option, with a
+`SENTRY_AUTH_TOKEN` environment variable, or with an `.env.sentry-build-plugin` file in the working directory when
+building your project. We recommend adding the auth token to your CI/CD environment as an environment variable.
+
+Learn more about configuring the plugin in our
+[Sentry Vite Plugin documentation](https://www.npmjs.com/package/@sentry/vite-plugin).
+
+```typescript
+import { defineConfig } from '@solidjs/start/config';
+import { withSentry } from '@sentry/solidstart';
+
+export default defineConfig(
+ withSentry(
+ {
+ // SolidStart config
+ middleware: './src/middleware.ts',
+ },
+ {
+ // Sentry `withSentry` options
+ org: process.env.SENTRY_ORG,
+ project: process.env.SENTRY_PROJECT,
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ debug: true,
+ // optional: if your `instrument.server.ts` file is not located inside `src`
+ instrumentation: './mypath/instrument.server.ts',
+ },
+ ),
+);
+```
+
+### 6. Run your application
Then run your app
```bash
-NODE_OPTIONS='--import=./instrument.server.mjs' yarn start
-# or
-NODE_OPTIONS='--require=./instrument.server.js' yarn start
+NODE_OPTIONS='--import=./.output/server/instrument.server.mjs' yarn start
```
+⚠️ **Note build presets** ⚠️
+Depending on [build preset](https://nitro.unjs.io/deploy), the location of `instrument.server.mjs` differs. To find out
+where `instrument.server.mjs` is located, monitor the build log output for
+
+```bash
+[Sentry SolidStart withSentry] Successfully created /my/project/path/.output/server/instrument.server.mjs.
+```
+
+⚠️ **Note for platforms without the ability to modify `NODE_OPTIONS` or use `--import`** ⚠️
+Depending on where the application is deployed to, it might not be possible to modify or use `NODE_OPTIONS` to import
+`instrument.server.mjs`.
+
+For such platforms, we offer the `experimental_basicServerTracing` flag to add a top level import of
+`instrument.server.mjs` to the server entry file.
+
+```typescript
+import { defineConfig } from '@solidjs/start/config';
+import { withSentry } from '@sentry/solidstart';
+
+export default defineConfig(
+ withSentry(
+ {
+ // ...
+ middleware: './src/middleware.ts',
+ },
+ {
+ org: process.env.SENTRY_ORG,
+ project: process.env.SENTRY_PROJECT,
+ authToken: process.env.SENTRY_AUTH_TOKEN,
+ debug: true,
+ // optional: if your `instrument.server.ts` file is not located inside `src`
+ instrumentation: './mypath/instrument.server.ts',
+ // optional: if NODE_OPTIONS or --import is not avaiable
+ experimental_basicServerTracing: true,
+ },
+ ),
+);
+```
+
+This has a **fundamental restriction**: It only supports limited performance instrumentation. **Only basic http
+instrumentation** will work, and no DB or framework-specific instrumentation will be available.
+
# Solid Router
The Solid Router instrumentation uses the Solid Router library to create navigation spans to ensure you collect
@@ -156,35 +234,3 @@ render(
document.getElementById('root'),
);
```
-
-## Uploading Source Maps
-
-To upload source maps, add the `sentrySolidStartVite` plugin from `@sentry/solidstart` to your `app.config.ts` and
-configure an auth token. Auth tokens can be passed to the plugin explicitly with the `authToken` option, with a
-`SENTRY_AUTH_TOKEN` environment variable, or with an `.env.sentry-build-plugin` file in the working directory when
-building your project. We recommend you add the auth token to your CI/CD environment as an environment variable.
-
-Learn more about configuring the plugin in our
-[Sentry Vite Plugin documentation](https://www.npmjs.com/package/@sentry/vite-plugin).
-
-```typescript
-// app.config.ts
-import { defineConfig } from '@solidjs/start/config';
-import { sentrySolidStartVite } from '@sentry/solidstart';
-
-export default defineConfig({
- // ...
-
- vite: {
- plugins: [
- sentrySolidStartVite({
- org: process.env.SENTRY_ORG,
- project: process.env.SENTRY_PROJECT,
- authToken: process.env.SENTRY_AUTH_TOKEN,
- debug: true,
- }),
- ],
- },
- // ...
-});
-```
diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts
new file mode 100644
index 000000000000..1aab37cfc157
--- /dev/null
+++ b/packages/solidstart/src/config/addInstrumentation.ts
@@ -0,0 +1,95 @@
+import * as fs from 'fs';
+import * as path from 'path';
+import { consoleSandbox } from '@sentry/utils';
+import type { Nitro } from './types';
+
+// 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.
+ *
+ * This will no-op if no `instrument.server.js` file was found in the
+ * build directory. Make sure the `sentrySolidStartVite` plugin was
+ * added to `app.config.ts` to enable building the instrumentation file.
+ */
+export async function addInstrumentationFileToBuild(nitro: Nitro): Promise {
+ // 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;
+ const source = path.resolve(buildDir, 'build', 'ssr', 'instrument.server.js');
+ const destination = path.resolve(serverDir, 'instrument.server.mjs');
+
+ try {
+ await fs.promises.copyFile(source, destination);
+
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.log(`[Sentry SolidStart withSentry] Successfully created ${destination}.`);
+ });
+ } catch (error) {
+ consoleSandbox(() => {
+ // eslint-disable-next-line no-console
+ console.warn(`[Sentry SolidStart withSentry] Failed to create ${destination}.`, 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 experimental_addInstrumentationFileTopLevelImportToServerEntry(
+ serverDir: string,
+ preset: string,
+): Promise {
+ // Static file hosts have no server component so there's nothing to do
+ if (staticHostPresets.includes(preset)) {
+ return;
+ }
+
+ const instrumentationFile = path.resolve(serverDir, 'instrument.server.mjs');
+ const serverEntryFileName = serverFilePresets.includes(preset) ? '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,
+ );
+ }
+}
diff --git a/packages/solidstart/src/config/index.ts b/packages/solidstart/src/config/index.ts
new file mode 100644
index 000000000000..4cf4b985c18a
--- /dev/null
+++ b/packages/solidstart/src/config/index.ts
@@ -0,0 +1,2 @@
+export * from './withSentry';
+export type { Nitro, SentrySolidStartConfigOptions } from './types';
diff --git a/packages/solidstart/src/config/types.ts b/packages/solidstart/src/config/types.ts
new file mode 100644
index 000000000000..2c67942c8a4d
--- /dev/null
+++ b/packages/solidstart/src/config/types.ts
@@ -0,0 +1,35 @@
+import type { defineConfig } from '@solidjs/start/config';
+// Types to avoid pulling in extra dependencies
+// These are non-exhaustive
+export type Nitro = {
+ options: {
+ buildDir: string;
+ output: {
+ serverDir: string;
+ };
+ preset: string;
+ };
+};
+
+export type SolidStartInlineConfig = Parameters[0];
+
+export type SolidStartInlineServerConfig = {
+ hooks?: {
+ close?: () => unknown;
+ 'rollup:before'?: (nitro: Nitro) => unknown;
+ };
+};
+
+export type SentrySolidStartConfigOptions = {
+ /**
+ * Enabling basic server tracing 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 this option is `true`, the Sentry SDK will import the instrumentation.server.ts|js file at the top of the server entry file to load the SDK on the server.
+ *
+ * **DO NOT** enable this option if you've already added the node option `--import` in your node start script. This would initialize Sentry twice on the server-side and leads to unexpected issues.
+ *
+ * @default false
+ */
+ experimental_basicServerTracing?: boolean;
+};
diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts
new file mode 100644
index 000000000000..a91d7e4458ad
--- /dev/null
+++ b/packages/solidstart/src/config/withSentry.ts
@@ -0,0 +1,63 @@
+import { addSentryPluginToVite } from '../vite';
+import type { SentrySolidStartPluginOptions } from '../vite/types';
+import {
+ addInstrumentationFileToBuild,
+ experimental_addInstrumentationFileTopLevelImportToServerEntry,
+} from './addInstrumentation';
+import type { Nitro, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types';
+
+/**
+ * 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 const withSentry = (
+ solidStartConfig: SolidStartInlineConfig = {},
+ sentrySolidStartPluginOptions: SentrySolidStartPluginOptions = {},
+): SolidStartInlineConfig => {
+ const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig;
+ const hooks = server.hooks || {};
+ const vite =
+ typeof solidStartConfig.vite === 'function'
+ ? (...args: unknown[]) => addSentryPluginToVite(solidStartConfig.vite(...args), sentrySolidStartPluginOptions)
+ : addSentryPluginToVite(solidStartConfig.vite, sentrySolidStartPluginOptions);
+
+ let serverDir: string;
+ let buildPreset: string;
+
+ return {
+ ...solidStartConfig,
+ vite,
+ server: {
+ ...server,
+ hooks: {
+ ...hooks,
+ async close() {
+ if (sentrySolidStartPluginOptions.experimental_basicServerTracing) {
+ await experimental_addInstrumentationFileTopLevelImportToServerEntry(serverDir, buildPreset);
+ }
+
+ // Run user provided hook
+ if (hooks.close) {
+ hooks.close();
+ }
+ },
+ async 'rollup:before'(nitro: Nitro) {
+ serverDir = nitro.options.output.serverDir;
+ buildPreset = nitro.options.preset;
+
+ await addInstrumentationFileToBuild(nitro);
+
+ // Run user provided hook
+ if (hooks['rollup:before']) {
+ hooks['rollup:before'](nitro);
+ }
+ },
+ },
+ },
+ };
+};
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 51adf848775a..13b9a6dd7432 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 { Integration, Options, StackParser } from '@sentry/types';
diff --git a/packages/solidstart/src/vite/buildInstrumentationFile.ts b/packages/solidstart/src/vite/buildInstrumentationFile.ts
new file mode 100644
index 000000000000..abb02e8d03ce
--- /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/utils';
+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..227a303b0ad4 100644
--- a/packages/solidstart/src/vite/sentrySolidStartVite.ts
+++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts
@@ -1,4 +1,5 @@
-import type { Plugin } from 'vite';
+import type { Plugin, UserConfig } from 'vite';
+import { makeBuildInstrumentationFilePlugin } from './buildInstrumentationFile';
import { makeSourceMapsVitePlugin } from './sourceMaps';
import type { SentrySolidStartPluginOptions } from './types';
@@ -14,5 +15,26 @@ export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}
}
}
+ // TODO: Ensure this file is source mapped too.
+ // Placing this after the sentry vite plugin means this
+ // file won't get a sourcemap and won't have a debug id injected.
+ // Because the file is just copied over to the output server
+ // directory the release injection file from sentry vite plugin
+ // wouldn't resolve correctly otherwise.
+ sentryPlugins.push(makeBuildInstrumentationFilePlugin(options));
+
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] : [];
+ plugins.unshift(sentrySolidStartVite(options));
+
+ return {
+ ...config,
+ plugins,
+ };
+};
diff --git a/packages/solidstart/src/vite/sourceMaps.ts b/packages/solidstart/src/vite/sourceMaps.ts
index 548038515e79..21dce8070c73 100644
--- a/packages/solidstart/src/vite/sourceMaps.ts
+++ b/packages/solidstart/src/vite/sourceMaps.ts
@@ -22,7 +22,7 @@ export function makeSourceMapsVitePlugin(options: SentrySolidStartPluginOptions)
if (!sourceMapsUploadOptions?.filesToDeleteAfterUpload) {
// eslint-disable-next-line no-console
console.warn(
- `[Sentry SolidStart PLugin] We recommend setting the \`sourceMapsUploadOptions.filesToDeleteAfterUpload\` option to clean up source maps after uploading.
+ `[Sentry SolidStart Plugin] We recommend setting the \`sourceMapsUploadOptions.filesToDeleteAfterUpload\` option to clean up source maps after uploading.
[Sentry SolidStart Plugin] Otherwise, source maps might be deployed to production, depending on your configuration`,
);
}
diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts
index 4a64e4856b5d..c31e901efc2e 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,24 @@ 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;
+
+ /**
+ * Enabling basic server tracing 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 this option is `true`, the Sentry SDK will import the instrumentation.server.ts|js file at the top of the server entry file to load the SDK on the server.
+ *
+ * **DO NOT** enable this option if you've already added the node option `--import` in your node start script. This would initialize Sentry twice on the server-side and leads to unexpected issues.
+ *
+ * @default false
+ */
+ experimental_basicServerTracing?: boolean;
};
diff --git a/packages/solidstart/test/config/addInstrumentation.test.ts b/packages/solidstart/test/config/addInstrumentation.test.ts
new file mode 100644
index 000000000000..7f20911a70b3
--- /dev/null
+++ b/packages/solidstart/test/config/addInstrumentation.test.ts
@@ -0,0 +1,135 @@
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Nitro } from '../../build/types/config/types';
+import {
+ addInstrumentationFileToBuild,
+ experimental_addInstrumentationFileTopLevelImportToServerEntry,
+ serverFilePresets,
+ staticHostPresets,
+} from '../../src/config/addInstrumentation';
+
+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();
+
+vi.mock('fs', async () => {
+ const actual = await vi.importActual('fs');
+ return {
+ ...actual,
+ 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),
+ },
+ };
+});
+
+beforeEach(() => {
+ vi.clearAllMocks();
+});
+
+describe('addInstrumentationFileToBuild()', () => {
+ const nitroOptions: Nitro = {
+ options: {
+ buildDir: '/path/to/buildDir',
+ output: {
+ serverDir: '/path/to/serverDir',
+ },
+ preset: 'vercel',
+ },
+ };
+
+ it('adds `instrument.server.mjs` to the server output directory', async () => {
+ fsCopyFileMock.mockResolvedValueOnce(true);
+ await addInstrumentationFileToBuild(nitroOptions);
+ expect(fsCopyFileMock).toHaveBeenCalledWith(
+ '/path/to/buildDir/build/ssr/instrument.server.js',
+ '/path/to/serverDir/instrument.server.mjs',
+ );
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Successfully created /path/to/serverDir/instrument.server.mjs.',
+ );
+ });
+
+ it('warns when `instrument.server.js` can not be copied to the server output directory', async () => {
+ const error = new Error('Failed to copy file.');
+ fsCopyFileMock.mockRejectedValueOnce(error);
+ await addInstrumentationFileToBuild(nitroOptions);
+ 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 create /path/to/serverDir/instrument.server.mjs.',
+ error,
+ );
+ });
+
+ it.each([staticHostPresets])("doesn't add `instrument.server.mjs` for static host `%s`", async preset => {
+ await addInstrumentationFileToBuild({
+ ...nitroOptions,
+ options: {
+ ...nitroOptions.options,
+ preset,
+ },
+ });
+ expect(fsCopyFileMock).not.toHaveBeenCalled();
+ });
+});
+
+describe('experimental_addInstrumentationFileTopLevelImportToServerEntry()', () => {
+ it('adds a top level import of `instrument.server.mjs` to the index.mjs entry file', async () => {
+ fsAccessMock.mockResolvedValueOnce(true);
+ fsReadFile.mockResolvedValueOnce("import process from 'node:process';");
+ fsWriteFileMock.mockResolvedValueOnce(true);
+ await experimental_addInstrumentationFileTopLevelImportToServerEntry('/path/to/serverDir', 'node_server');
+ expect(fsWriteFileMock).toHaveBeenCalledWith(
+ '/path/to/serverDir/index.mjs',
+ "import './instrument.server.mjs';\nimport process from 'node:process';",
+ );
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Added `/path/to/serverDir/instrument.server.mjs` as top level import to `/path/to/serverDir/index.mjs`.',
+ );
+ });
+
+ it.each([serverFilePresets])(
+ 'adds a top level import of `instrument.server.mjs` to the server.mjs entry file for preset `%s`',
+ async preset => {
+ fsAccessMock.mockResolvedValueOnce(true);
+ fsReadFile.mockResolvedValueOnce("import process from 'node:process';");
+ fsWriteFileMock.mockResolvedValueOnce(true);
+ await experimental_addInstrumentationFileTopLevelImportToServerEntry('/path/to/serverDir', preset);
+ expect(fsWriteFileMock).toHaveBeenCalledWith(
+ '/path/to/serverDir/server.mjs',
+ "import './instrument.server.mjs';\nimport process from 'node:process';",
+ );
+ expect(consoleLogSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Added `/path/to/serverDir/instrument.server.mjs` as top level import to `/path/to/serverDir/server.mjs`.',
+ );
+ },
+ );
+
+ it("doesn't modify the sever entry file if `instrumentation.server.mjs` is not found", async () => {
+ const error = new Error('File not found.');
+ fsAccessMock.mockRejectedValueOnce(error);
+ await experimental_addInstrumentationFileTopLevelImportToServerEntry('/path/to/serverDir', 'node_server');
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ '[Sentry SolidStart withSentry] Failed to add `/path/to/serverDir/instrument.server.mjs` as top level import to `/path/to/serverDir/index.mjs`.',
+ error,
+ );
+ });
+
+ it.each([staticHostPresets])(
+ "doesn't import `instrument.server.mjs` as top level import for host `%s`",
+ async preset => {
+ fsAccessMock.mockResolvedValueOnce(true);
+ await experimental_addInstrumentationFileTopLevelImportToServerEntry('/path/to/serverDir', preset);
+ expect(fsWriteFileMock).not.toHaveBeenCalled();
+ },
+ );
+});
diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts
new file mode 100644
index 000000000000..52ebb2449c25
--- /dev/null
+++ b/packages/solidstart/test/config/withSentry.test.ts
@@ -0,0 +1,146 @@
+import type { Plugin } from 'vite';
+import { beforeEach, describe, expect, it, vi } from 'vitest';
+import type { Nitro } from '../../src/config';
+import { withSentry } from '../../src/config';
+
+const userDefinedNitroRollupBeforeHookMock = vi.fn();
+const userDefinedNitroCloseHookMock = vi.fn();
+const addInstrumentationFileToBuildMock = vi.fn();
+const experimental_addInstrumentationFileTopLevelImportToServerEntryMock = vi.fn();
+
+vi.mock('../../src/config/addInstrumentation', () => ({
+ addInstrumentationFileToBuild: (...args: unknown[]) => addInstrumentationFileToBuildMock(...args),
+ experimental_addInstrumentationFileTopLevelImportToServerEntry: (...args: unknown[]) =>
+ experimental_addInstrumentationFileTopLevelImportToServerEntryMock(...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', 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', async () => {
+ const config = withSentry(solidStartConfig, { experimental_basicServerTracing: true });
+ await config?.server.hooks['rollup:before'](nitroOptions);
+ await config?.server.hooks['close'](nitroOptions);
+ expect(experimental_addInstrumentationFileTopLevelImportToServerEntryMock).toHaveBeenCalledWith(
+ '/path/to/serverDir',
+ 'vercel',
+ );
+ expect(userDefinedNitroCloseHookMock).toHaveBeenCalled();
+ });
+
+ it('does not add the instrumentation file as top level import if experimental flag was not true', async () => {
+ const config = withSentry(solidStartConfig, { experimental_basicServerTracing: false });
+ await config?.server.hooks['rollup:before'](nitroOptions);
+ await config?.server.hooks['close'](nitroOptions);
+ expect(experimental_addInstrumentationFileTopLevelImportToServerEntryMock).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-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',
+ 'sentry-solidstart-build-instrumentation-file',
+ ]);
+ });
+
+ 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-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',
+ 'sentry-solidstart-build-instrumentation-file',
+ '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-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',
+ 'sentry-solidstart-build-instrumentation-file',
+ '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..45faa8b797f9 100644
--- a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts
+++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts
@@ -30,20 +30,23 @@ describe('sentrySolidStartVite()', () => {
'sentry-vite-debug-id-injection-plugin',
'sentry-vite-debug-id-upload-plugin',
'sentry-file-deletion-plugin',
+ 'sentry-solidstart-build-instrumentation-file',
]);
});
- 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;
});