From 9c033b5d978390c66a85b9dbbcae4a2862f50ec6 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 24 Sep 2024 12:00:54 +0900 Subject: [PATCH 01/20] feat(solidstart): Add `withSentry` config wrapper to enable building instrumentation files --- packages/solidstart/.eslintrc.js | 7 ++ .../src/config/addInstrumentation.ts | 82 +++++++++++++ packages/solidstart/src/config/index.ts | 0 packages/solidstart/src/config/types.ts | 35 ++++++ packages/solidstart/src/config/withSentry.ts | 47 ++++++++ .../src/vite/buildInstrumentationFile.ts | 55 +++++++++ .../src/vite/sentrySolidStartVite.ts | 3 + packages/solidstart/src/vite/types.ts | 8 ++ .../test/vite/buildInstrumentation.test.ts | 114 ++++++++++++++++++ .../test/vite/sentrySolidStartVite.test.ts | 11 +- 10 files changed, 358 insertions(+), 4 deletions(-) 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/withSentry.ts create mode 100644 packages/solidstart/src/vite/buildInstrumentationFile.ts create mode 100644 packages/solidstart/test/vite/buildInstrumentation.test.ts diff --git a/packages/solidstart/.eslintrc.js b/packages/solidstart/.eslintrc.js index a22f9710cf6b..0fe78630b548 100644 --- a/packages/solidstart/.eslintrc.js +++ b/packages/solidstart/.eslintrc.js @@ -10,6 +10,13 @@ module.exports = { project: ['tsconfig.test.json'], }, }, + { + files: ['src/vite/**', 'src/server/**', 'src/config/**'], + rules: { + '@sentry-internal/sdk/no-optional-chaining': 'off', + '@sentry-internal/sdk/no-nullish-coalescing': 'off', + }, + }, ], extends: ['../../.eslintrc.js'], }; diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts new file mode 100644 index 000000000000..05e5477ae6c4 --- /dev/null +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -0,0 +1,82 @@ +import * as fs from 'fs'; +import * as path from 'path'; +import { consoleSandbox } from '@sentry/utils'; +import type { Nitro } from './types'; + +/** + * 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 { + const { buildDir, serverDir } = nitro.options.output; + const source = path.join(buildDir, 'build', 'ssr', 'instrument.server.js'); + const destination = path.join(serverDir, 'instrument.server.mjs'); + + try { + await fs.promises.access(source, fs.constants.F_OK); + 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 { + // other presets ('node-server' or 'vercel') have an index.mjs + const presetsWithServerFile = ['netlify']; + const instrumentationFile = path.join(serverDir, 'instrument.server.mjs'); + const serverEntryFileName = presetsWithServerFile.includes(preset) ? 'server.mjs' : 'index.mjs'; + const serverEntryFile = path.join(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] Tried 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..e69de29bb2d1 diff --git a/packages/solidstart/src/config/types.ts b/packages/solidstart/src/config/types.ts new file mode 100644 index 000000000000..3ffa25aeaf0b --- /dev/null +++ b/packages/solidstart/src/config/types.ts @@ -0,0 +1,35 @@ +// Types to avoid pulling in extra dependencies +// These are non-exhaustive +export type Nitro = { + options: { + buildDir: string; + output: { + buildDir: string; + serverDir: string; + }; + preset: string; + }; +}; + +export type SolidStartInlineConfig = { + server?: { + 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..921eca950889 --- /dev/null +++ b/packages/solidstart/src/config/withSentry.ts @@ -0,0 +1,47 @@ +import { + addInstrumentationFileToBuild, + experimental_addInstrumentationFileTopLevelImportToServerEntry, +} from './addInstrumentation'; +import type { SentrySolidStartConfigOptions, SolidStartInlineConfig } from './types'; + +export const withSentry = ( + solidStartConfig: SolidStartInlineConfig = {}, + sentrySolidStartConfigOptions: SentrySolidStartConfigOptions = {}, +): SolidStartInlineConfig => { + const server = solidStartConfig.server || {}; + const hooks = server.hooks || {}; + + let serverDir: string; + let buildPreset: string; + + return { + ...solidStartConfig, + server: { + ...server, + hooks: { + ...hooks, + async close() { + if (sentrySolidStartConfigOptions.experimental_basicServerTracing) { + await experimental_addInstrumentationFileTopLevelImportToServerEntry(serverDir, buildPreset); + } + + // Run user provided hook + if (hooks.close) { + hooks.close(); + } + }, + async 'rollup:before'(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/vite/buildInstrumentationFile.ts b/packages/solidstart/src/vite/buildInstrumentationFile.ts new file mode 100644 index 000000000000..a25546e15c94 --- /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'; + +/** + * A Sentry plugin for SolidStart to build the server + * `instrument.server.ts` file. + */ +export function makeBuildInstrumentationFilePlugin( + instrumentationFilePath: string = './src/instrument.server.ts', +): Plugin { + return { + name: 'sentry-solidstart-build-instrumentation-file', + apply: 'build', + enforce: 'post', + async config(config: UserConfig, { command }) { + 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.join(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..c8332373bc58 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 { makeBuildInstrumentationFilePlugin } from './buildInstrumentationFile'; import { makeSourceMapsVitePlugin } from './sourceMaps'; import type { SentrySolidStartPluginOptions } from './types'; @@ -8,6 +9,8 @@ import type { SentrySolidStartPluginOptions } from './types'; export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}): Plugin[] => { const sentryPlugins: Plugin[] = []; + sentryPlugins.push(makeBuildInstrumentationFilePlugin(options.instrumentation)); + if (process.env.NODE_ENV !== 'development') { if (options.sourceMapsUploadOptions?.enabled ?? true) { sentryPlugins.push(...makeSourceMapsVitePlugin(options)); diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index 4a64e4856b5d..a725478aae7b 100644 --- a/packages/solidstart/src/vite/types.ts +++ b/packages/solidstart/src/vite/types.ts @@ -125,4 +125,12 @@ export type SentrySolidStartPluginOptions = { * Enabling this will give you, for example logs about source maps. */ debug?: boolean; + + /** + * The path to your `instrumentation.server.ts|js` file. + * e.g. './src/instrumentation.server.ts` + * + * Defaults to: `./src/instrumentation.server.ts` + */ + instrumentation?: string; }; diff --git a/packages/solidstart/test/vite/buildInstrumentation.test.ts b/packages/solidstart/test/vite/buildInstrumentation.test.ts new file mode 100644 index 000000000000..c05409de6e7f --- /dev/null +++ b/packages/solidstart/test/vite/buildInstrumentation.test.ts @@ -0,0 +1,114 @@ +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(); + 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('./src/myapp/instrument.server.ts'); + 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(); + 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(); + 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(); + 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(); + 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(); + 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..8915c5a70671 100644 --- a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts +++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts @@ -23,6 +23,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 +34,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; }); From ae5efcd0663ddfa789df4fb6b81dfd76c67042ab Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 24 Sep 2024 15:14:23 +0900 Subject: [PATCH 02/20] Fix typing, unify vite plugin options --- .../src/config/addInstrumentation.ts | 11 ++++++----- packages/solidstart/src/config/index.ts | 1 + packages/solidstart/src/config/types.ts | 13 +++++++------ packages/solidstart/src/config/withSentry.ts | 11 ++++++++--- packages/solidstart/src/index.server.ts | 1 + packages/solidstart/src/index.types.ts | 1 + .../src/vite/buildInstrumentationFile.ts | 8 ++++---- .../src/vite/sentrySolidStartVite.ts | 2 +- .../test/vite/buildInstrumentation.test.ts | 18 +++++++++++++++++- 9 files changed, 46 insertions(+), 20 deletions(-) diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts index 05e5477ae6c4..2e065e67ef19 100644 --- a/packages/solidstart/src/config/addInstrumentation.ts +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -11,9 +11,10 @@ import type { Nitro } from './types'; * added to `app.config.ts` to enable building the instrumentation file. */ export async function addInstrumentationFileToBuild(nitro: Nitro): Promise { - const { buildDir, serverDir } = nitro.options.output; - const source = path.join(buildDir, 'build', 'ssr', 'instrument.server.js'); - const destination = path.join(serverDir, 'instrument.server.mjs'); + 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.access(source, fs.constants.F_OK); @@ -44,9 +45,9 @@ export async function experimental_addInstrumentationFileTopLevelImportToServerE ): Promise { // other presets ('node-server' or 'vercel') have an index.mjs const presetsWithServerFile = ['netlify']; - const instrumentationFile = path.join(serverDir, 'instrument.server.mjs'); + const instrumentationFile = path.resolve(serverDir, 'instrument.server.mjs'); const serverEntryFileName = presetsWithServerFile.includes(preset) ? 'server.mjs' : 'index.mjs'; - const serverEntryFile = path.join(serverDir, serverEntryFileName); + const serverEntryFile = path.resolve(serverDir, serverEntryFileName); try { await fs.promises.access(instrumentationFile, fs.constants.F_OK); diff --git a/packages/solidstart/src/config/index.ts b/packages/solidstart/src/config/index.ts index e69de29bb2d1..4949f4bdf523 100644 --- a/packages/solidstart/src/config/index.ts +++ 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 index 3ffa25aeaf0b..eb7845fc3b6c 100644 --- a/packages/solidstart/src/config/types.ts +++ b/packages/solidstart/src/config/types.ts @@ -1,3 +1,4 @@ +import type { defineConfig } from '@solidjs/start/config'; // Types to avoid pulling in extra dependencies // These are non-exhaustive export type Nitro = { @@ -11,12 +12,12 @@ export type Nitro = { }; }; -export type SolidStartInlineConfig = { - server?: { - hooks?: { - close?: () => unknown; - 'rollup:before'?: (nitro: Nitro) => unknown; - }; +export type SolidStartInlineConfig = Parameters[0]; + +export type SolidStartInlineConfigNitroHooks = { + hooks?: { + close?: () => unknown; + 'rollup:before'?: (nitro: Nitro) => unknown; }; }; diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index 921eca950889..d1ecb05bd7af 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -2,13 +2,18 @@ import { addInstrumentationFileToBuild, experimental_addInstrumentationFileTopLevelImportToServerEntry, } from './addInstrumentation'; -import type { SentrySolidStartConfigOptions, SolidStartInlineConfig } from './types'; +import type { + Nitro, + SentrySolidStartConfigOptions, + SolidStartInlineConfig, + SolidStartInlineConfigNitroHooks, +} from './types'; export const withSentry = ( solidStartConfig: SolidStartInlineConfig = {}, sentrySolidStartConfigOptions: SentrySolidStartConfigOptions = {}, ): SolidStartInlineConfig => { - const server = solidStartConfig.server || {}; + const server = (solidStartConfig.server || {}) as SolidStartInlineConfigNitroHooks; const hooks = server.hooks || {}; let serverDir: string; @@ -30,7 +35,7 @@ export const withSentry = ( hooks.close(); } }, - async 'rollup:before'(nitro) { + async 'rollup:before'(nitro: Nitro) { serverDir = nitro.options.output.serverDir; buildPreset = nitro.options.preset; 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 fb5b221086e7..39f9831c543c 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 index a25546e15c94..abb02e8d03ce 100644 --- a/packages/solidstart/src/vite/buildInstrumentationFile.ts +++ b/packages/solidstart/src/vite/buildInstrumentationFile.ts @@ -2,19 +2,19 @@ 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( - instrumentationFilePath: string = './src/instrument.server.ts', -): Plugin { +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 || {}; @@ -38,7 +38,7 @@ export function makeBuildInstrumentationFilePlugin( return config; } - input.push(path.join(router.root, instrumentationFilePath)); + input.push(path.resolve(router.root, instrumentationFilePath)); return { ...config, diff --git a/packages/solidstart/src/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts index c8332373bc58..70df00c1fdaf 100644 --- a/packages/solidstart/src/vite/sentrySolidStartVite.ts +++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts @@ -9,7 +9,7 @@ import type { SentrySolidStartPluginOptions } from './types'; export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}): Plugin[] => { const sentryPlugins: Plugin[] = []; - sentryPlugins.push(makeBuildInstrumentationFilePlugin(options.instrumentation)); + sentryPlugins.push(makeBuildInstrumentationFilePlugin(options)); if (process.env.NODE_ENV !== 'development') { if (options.sourceMapsUploadOptions?.enabled ?? true) { diff --git a/packages/solidstart/test/vite/buildInstrumentation.test.ts b/packages/solidstart/test/vite/buildInstrumentation.test.ts index c05409de6e7f..52378a668870 100644 --- a/packages/solidstart/test/vite/buildInstrumentation.test.ts +++ b/packages/solidstart/test/vite/buildInstrumentation.test.ts @@ -47,18 +47,26 @@ describe('makeBuildInstrumentationFilePlugin()', () => { 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('./src/myapp/instrument.server.ts'); + 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, @@ -74,6 +82,8 @@ describe('makeBuildInstrumentationFilePlugin()', () => { 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, @@ -89,6 +99,8 @@ describe('makeBuildInstrumentationFilePlugin()', () => { 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'); }); @@ -96,6 +108,8 @@ describe('makeBuildInstrumentationFilePlugin()', () => { 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); }); @@ -104,6 +118,8 @@ describe('makeBuildInstrumentationFilePlugin()', () => { 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( From 4764548d78620ad1557ec85ad7398ad6c93f8220 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 24 Sep 2024 15:19:49 +0900 Subject: [PATCH 03/20] Add JSDoc to `withSentry` --- packages/solidstart/src/config/withSentry.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index d1ecb05bd7af..d27d3028fc50 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -9,6 +9,15 @@ import type { SolidStartInlineConfigNitroHooks, } 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 sentrySolidStartConfigOptions Options to configure the plugin + * @returns The modified config to be exported and passed back into `defineConfig` + */ export const withSentry = ( solidStartConfig: SolidStartInlineConfig = {}, sentrySolidStartConfigOptions: SentrySolidStartConfigOptions = {}, From 1677499f90dc29c92c975e03cf487b601194b22d Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 25 Sep 2024 12:48:17 +0900 Subject: [PATCH 04/20] Add unit tests for config helpers --- .../src/config/addInstrumentation.ts | 22 ++- packages/solidstart/src/config/types.ts | 1 - .../test/config/addInstrumentation.test.ts | 135 ++++++++++++++++++ .../solidstart/test/config/withSentry.test.ts | 65 +++++++++ 4 files changed, 217 insertions(+), 6 deletions(-) create mode 100644 packages/solidstart/test/config/addInstrumentation.test.ts create mode 100644 packages/solidstart/test/config/withSentry.test.ts diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts index 2e065e67ef19..1aab37cfc157 100644 --- a/packages/solidstart/src/config/addInstrumentation.ts +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -3,6 +3,11 @@ 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. * @@ -11,13 +16,17 @@ import type { Nitro } from './types'; * 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.access(source, fs.constants.F_OK); await fs.promises.copyFile(source, destination); consoleSandbox(() => { @@ -43,10 +52,13 @@ export async function experimental_addInstrumentationFileTopLevelImportToServerE serverDir: string, preset: string, ): Promise { - // other presets ('node-server' or 'vercel') have an index.mjs - const presetsWithServerFile = ['netlify']; + // 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 = presetsWithServerFile.includes(preset) ? 'server.mjs' : 'index.mjs'; + const serverEntryFileName = serverFilePresets.includes(preset) ? 'server.mjs' : 'index.mjs'; const serverEntryFile = path.resolve(serverDir, serverEntryFileName); try { @@ -55,7 +67,7 @@ export async function experimental_addInstrumentationFileTopLevelImportToServerE consoleSandbox(() => { // eslint-disable-next-line no-console console.warn( - `[Sentry SolidStart withSentry] Tried to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`, + `[Sentry SolidStart withSentry] Failed to add \`${instrumentationFile}\` as top level import to \`${serverEntryFile}\`.`, error, ); }); diff --git a/packages/solidstart/src/config/types.ts b/packages/solidstart/src/config/types.ts index eb7845fc3b6c..e9623313f438 100644 --- a/packages/solidstart/src/config/types.ts +++ b/packages/solidstart/src/config/types.ts @@ -5,7 +5,6 @@ export type Nitro = { options: { buildDir: string; output: { - buildDir: string; serverDir: string; }; preset: 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..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..faf54e4e4679 --- /dev/null +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -0,0 +1,65 @@ +import { beforeEach, describe, it, vi } from 'vitest'; +import type { Nitro } from '../../build/types/config/types'; +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(); + }); +}); From 25d191cd1c1eb8b918935ad86fb701fc87146f12 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 25 Sep 2024 13:45:25 +0900 Subject: [PATCH 05/20] Change ordering of plugins --- packages/solidstart/src/vite/sentrySolidStartVite.ts | 10 ++++++++-- .../solidstart/test/vite/sentrySolidStartVite.test.ts | 2 +- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/packages/solidstart/src/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts index 70df00c1fdaf..35a7da456b06 100644 --- a/packages/solidstart/src/vite/sentrySolidStartVite.ts +++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts @@ -9,13 +9,19 @@ import type { SentrySolidStartPluginOptions } from './types'; export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}): Plugin[] => { const sentryPlugins: Plugin[] = []; - sentryPlugins.push(makeBuildInstrumentationFilePlugin(options)); - if (process.env.NODE_ENV !== 'development') { if (options.sourceMapsUploadOptions?.enabled ?? true) { sentryPlugins.push(...makeSourceMapsVitePlugin(options)); } } + // 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; }; diff --git a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts index 8915c5a70671..45faa8b797f9 100644 --- a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts +++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts @@ -23,7 +23,6 @@ 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', @@ -31,6 +30,7 @@ describe('sentrySolidStartVite()', () => { 'sentry-vite-debug-id-injection-plugin', 'sentry-vite-debug-id-upload-plugin', 'sentry-file-deletion-plugin', + 'sentry-solidstart-build-instrumentation-file', ]); }); From 9688b976666b087f4fc6901c3335fda0ef817298 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 25 Sep 2024 14:32:56 +0900 Subject: [PATCH 06/20] Update README --- packages/solidstart/README.md | 133 +++++++++++++++++++++++++--------- 1 file changed, 97 insertions(+), 36 deletions(-) diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index c43ac54c7037..7734eb06cb74 100644 --- a/packages/solidstart/README.md +++ b/packages/solidstart/README.md @@ -101,16 +101,109 @@ 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. + +#### 5.1 Wrapping the config with `withSentry` + +Add `withSentry` from `@sentry/solidstart` and wrap SolidStart's config inside `app.config.ts`. + +```typescript +import { defineConfig } from '@solidjs/start/config' +import { withSentry } from "@sentry/solidstart"; + +export default defineConfig(withSentry({ + // ... + middleware: './src/middleware.ts', +})) + +``` + +#### 5.2 Generate source maps and build `instrument.server.ts` + +Sentry relies on running `instrument.server.ts` as early as possible. Add the `sentrySolidStartVite` plugin +from `@sentry/solidstart` to your `app.config.ts`. This takes care of building `instrument.server.ts` and placing it alongside the server entry file. + +To upload source maps, 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, withSentry } from '@sentry/solidstart'; + +export default defineConfig(withSentry({ + // ... + middleware: './src/middleware.ts', + vite: { + plugins: [ + sentrySolidStartVite({ + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + debug: true, + }), + ], + }, + // ... +})); +``` + +### 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 +// app.config.ts +import { defineConfig } from '@solidjs/start/config'; +import { sentrySolidStartVite, withSentry } from '@sentry/solidstart'; + +export default defineConfig(withSentry({ + // ... + middleware: './src/middleware.ts', + vite: { + plugins: [ + sentrySolidStartVite({ + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + debug: true, + }), + ], + }, + // ... +}, { 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 +249,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, - }), - ], - }, - // ... -}); -``` From 9123819ef1ac9d00232c521533f44fdeac3a4a95 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Wed, 25 Sep 2024 14:42:36 +0900 Subject: [PATCH 07/20] Document optional instrumentation file path option --- packages/solidstart/README.md | 119 ++++++++++--------- packages/solidstart/src/config/types.ts | 2 +- packages/solidstart/src/config/withSentry.ts | 4 +- packages/solidstart/src/vite/types.ts | 6 +- 4 files changed, 72 insertions(+), 59 deletions(-) diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index 7734eb06cb74..08b41b76361d 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'; @@ -110,24 +110,30 @@ For Sentry to work properly, SolidStart's `app.config.ts` has to be modified. Add `withSentry` from `@sentry/solidstart` and wrap SolidStart's config inside `app.config.ts`. ```typescript -import { defineConfig } from '@solidjs/start/config' -import { withSentry } from "@sentry/solidstart"; - -export default defineConfig(withSentry({ - // ... - middleware: './src/middleware.ts', -})) +import { defineConfig } from '@solidjs/start/config'; +import { withSentry } from '@sentry/solidstart'; +export default defineConfig( + withSentry({ + // ... + middleware: './src/middleware.ts', + }), +); ``` #### 5.2 Generate source maps and build `instrument.server.ts` -Sentry relies on running `instrument.server.ts` as early as possible. Add the `sentrySolidStartVite` plugin -from `@sentry/solidstart` to your `app.config.ts`. This takes care of building `instrument.server.ts` and placing it alongside the server entry file. +Sentry relies on running `instrument.server.ts` as early as possible. Add the `sentrySolidStartVite` plugin from +`@sentry/solidstart` to your `app.config.ts`. This takes care of building `instrument.server.ts` and placing it +alongside the server entry file. -To upload source maps, 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. +If your `instrument.server.ts` file is not located in the `src` folder, you can specify the path via the +`sentrySolidStartVite` plugin. + +To upload source maps, 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). @@ -137,21 +143,25 @@ Learn more about configuring the plugin in our import { defineConfig } from '@solidjs/start/config'; import { sentrySolidStartVite, withSentry } from '@sentry/solidstart'; -export default defineConfig(withSentry({ - // ... - middleware: './src/middleware.ts', - vite: { - plugins: [ - sentrySolidStartVite({ - org: process.env.SENTRY_ORG, - project: process.env.SENTRY_PROJECT, - authToken: process.env.SENTRY_AUTH_TOKEN, - debug: true, - }), - ], - }, - // ... -})); +export default defineConfig( + withSentry({ + // ... + middleware: './src/middleware.ts', + vite: { + plugins: [ + sentrySolidStartVite({ + 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 @@ -163,46 +173,49 @@ 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 +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`. +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. +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 // app.config.ts import { defineConfig } from '@solidjs/start/config'; import { sentrySolidStartVite, withSentry } from '@sentry/solidstart'; -export default defineConfig(withSentry({ - // ... - middleware: './src/middleware.ts', - vite: { - plugins: [ - sentrySolidStartVite({ - org: process.env.SENTRY_ORG, - project: process.env.SENTRY_PROJECT, - authToken: process.env.SENTRY_AUTH_TOKEN, - debug: true, - }), - ], - }, - // ... -}, { experimental_basicServerTracing: true })); +export default defineConfig( + withSentry( + { + // ... + middleware: './src/middleware.ts', + vite: { + plugins: [ + sentrySolidStartVite({ + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + debug: true, + }), + ], + }, + // ... + }, + { 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. - +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 diff --git a/packages/solidstart/src/config/types.ts b/packages/solidstart/src/config/types.ts index e9623313f438..2c67942c8a4d 100644 --- a/packages/solidstart/src/config/types.ts +++ b/packages/solidstart/src/config/types.ts @@ -13,7 +13,7 @@ export type Nitro = { export type SolidStartInlineConfig = Parameters[0]; -export type SolidStartInlineConfigNitroHooks = { +export type SolidStartInlineServerConfig = { hooks?: { close?: () => unknown; 'rollup:before'?: (nitro: Nitro) => unknown; diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index d27d3028fc50..3e2e631af8be 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -6,7 +6,7 @@ import type { Nitro, SentrySolidStartConfigOptions, SolidStartInlineConfig, - SolidStartInlineConfigNitroHooks, + SolidStartInlineServerConfig, } from './types'; /** @@ -22,7 +22,7 @@ export const withSentry = ( solidStartConfig: SolidStartInlineConfig = {}, sentrySolidStartConfigOptions: SentrySolidStartConfigOptions = {}, ): SolidStartInlineConfig => { - const server = (solidStartConfig.server || {}) as SolidStartInlineConfigNitroHooks; + const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig; const hooks = server.hooks || {}; let serverDir: string; diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index a725478aae7b..fdf252471de1 100644 --- a/packages/solidstart/src/vite/types.ts +++ b/packages/solidstart/src/vite/types.ts @@ -127,10 +127,10 @@ export type SentrySolidStartPluginOptions = { debug?: boolean; /** - * The path to your `instrumentation.server.ts|js` file. - * e.g. './src/instrumentation.server.ts` + * The path to your `instrument.server.ts|js` file. + * e.g. `./src/instrument.server.ts` * - * Defaults to: `./src/instrumentation.server.ts` + * Defaults to: `./src/instrument.server.ts` */ instrumentation?: string; }; From fd9678e18813d0fe17d822b67a126cb9c94731b9 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 27 Sep 2024 12:30:48 +0900 Subject: [PATCH 08/20] Add `sentrySolidStartVite` plugin when using `withSentry` --- packages/solidstart/src/config/index.ts | 1 + packages/solidstart/src/config/withSentry.ts | 20 +++-- .../src/vite/sentrySolidStartVite.ts | 15 +++- packages/solidstart/src/vite/types.ts | 14 ++- .../solidstart/test/config/withSentry.test.ts | 85 ++++++++++++++++++- 5 files changed, 122 insertions(+), 13 deletions(-) diff --git a/packages/solidstart/src/config/index.ts b/packages/solidstart/src/config/index.ts index 4949f4bdf523..4cf4b985c18a 100644 --- a/packages/solidstart/src/config/index.ts +++ b/packages/solidstart/src/config/index.ts @@ -1 +1,2 @@ export * from './withSentry'; +export type { Nitro, SentrySolidStartConfigOptions } from './types'; diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index 3e2e631af8be..a91d7e4458ad 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -1,13 +1,10 @@ +import { addSentryPluginToVite } from '../vite'; +import type { SentrySolidStartPluginOptions } from '../vite/types'; import { addInstrumentationFileToBuild, experimental_addInstrumentationFileTopLevelImportToServerEntry, } from './addInstrumentation'; -import type { - Nitro, - SentrySolidStartConfigOptions, - SolidStartInlineConfig, - SolidStartInlineServerConfig, -} from './types'; +import type { Nitro, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types'; /** * Modifies the passed in Solid Start configuration with build-time enhancements such as @@ -15,27 +12,32 @@ import type { * build preset. * * @param solidStartConfig A Solid Start configuration object, as usually passed to `defineConfig` in `app.config.ts|js` - * @param sentrySolidStartConfigOptions Options to configure the plugin + * @param sentrySolidStartPluginOptions Options to configure the plugin * @returns The modified config to be exported and passed back into `defineConfig` */ export const withSentry = ( solidStartConfig: SolidStartInlineConfig = {}, - sentrySolidStartConfigOptions: SentrySolidStartConfigOptions = {}, + 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 (sentrySolidStartConfigOptions.experimental_basicServerTracing) { + if (sentrySolidStartPluginOptions.experimental_basicServerTracing) { await experimental_addInstrumentationFileTopLevelImportToServerEntry(serverDir, buildPreset); } diff --git a/packages/solidstart/src/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts index 35a7da456b06..227a303b0ad4 100644 --- a/packages/solidstart/src/vite/sentrySolidStartVite.ts +++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts @@ -1,4 +1,4 @@ -import type { Plugin } from 'vite'; +import type { Plugin, UserConfig } from 'vite'; import { makeBuildInstrumentationFilePlugin } from './buildInstrumentationFile'; import { makeSourceMapsVitePlugin } from './sourceMaps'; import type { SentrySolidStartPluginOptions } from './types'; @@ -25,3 +25,16 @@ 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] : []; + plugins.unshift(sentrySolidStartVite(options)); + + return { + ...config, + plugins, + }; +}; diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index fdf252471de1..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 = { /** @@ -133,4 +133,16 @@ export type SentrySolidStartPluginOptions = { * 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/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts index faf54e4e4679..52ebb2449c25 100644 --- a/packages/solidstart/test/config/withSentry.test.ts +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -1,5 +1,6 @@ -import { beforeEach, describe, it, vi } from 'vitest'; -import type { Nitro } from '../../build/types/config/types'; +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(); @@ -62,4 +63,84 @@ describe('withSentry()', () => { 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', + ]); + }); }); From c4b50dd228d44036db55ce0758fe29132aeb62d4 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 27 Sep 2024 12:47:19 +0900 Subject: [PATCH 09/20] Update README --- packages/solidstart/README.md | 92 ++++++++++++----------------------- 1 file changed, 32 insertions(+), 60 deletions(-) diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index 08b41b76361d..0a410384bae0 100644 --- a/packages/solidstart/README.md +++ b/packages/solidstart/README.md @@ -103,64 +103,39 @@ the client and server. ### 5. Configure your application -For Sentry to work properly, SolidStart's `app.config.ts` has to be modified. - -#### 5.1 Wrapping the config with `withSentry` - -Add `withSentry` from `@sentry/solidstart` and wrap SolidStart's config inside `app.config.ts`. - -```typescript -import { defineConfig } from '@solidjs/start/config'; -import { withSentry } from '@sentry/solidstart'; - -export default defineConfig( - withSentry({ - // ... - middleware: './src/middleware.ts', - }), -); -``` - -#### 5.2 Generate source maps and build `instrument.server.ts` - -Sentry relies on running `instrument.server.ts` as early as possible. Add the `sentrySolidStartVite` plugin from -`@sentry/solidstart` to your `app.config.ts`. This takes care of building `instrument.server.ts` and placing it -alongside the server entry file. +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 -`sentrySolidStartVite` plugin. +`instrumentation` option to `withSentry`. -To upload source maps, 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. +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 -// app.config.ts import { defineConfig } from '@solidjs/start/config'; -import { sentrySolidStartVite, withSentry } from '@sentry/solidstart'; +import { withSentry } from '@sentry/solidstart'; export default defineConfig( - withSentry({ - // ... - middleware: './src/middleware.ts', - vite: { - plugins: [ - sentrySolidStartVite({ - 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', - }), - ], + 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', }, - // ... - }), + ), ); ``` @@ -188,28 +163,25 @@ For such platforms, we offer the `experimental_basicServerTracing` flag to add a `instrument.server.mjs` to the server entry file. ```typescript -// app.config.ts import { defineConfig } from '@solidjs/start/config'; -import { sentrySolidStartVite, withSentry } from '@sentry/solidstart'; +import { withSentry } from '@sentry/solidstart'; export default defineConfig( withSentry( { // ... middleware: './src/middleware.ts', - vite: { - plugins: [ - sentrySolidStartVite({ - org: process.env.SENTRY_ORG, - project: process.env.SENTRY_PROJECT, - authToken: process.env.SENTRY_AUTH_TOKEN, - debug: true, - }), - ], - }, - // ... }, - { experimental_basicServerTracing: true }, + { + 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, + }, ), ); ``` From 99e790ae3a036baa13d62930e242774c491ffd43 Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Fri, 27 Sep 2024 15:53:12 +0900 Subject: [PATCH 10/20] Add reload workoarund for dev server hydration error :( --- .../solidstart/app.config.ts | 27 ++++++++++++++----- .../solidstart/tests/errorboundary.test.ts | 2 ++ 2 files changed, 23 insertions(+), 6 deletions(-) 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; From d2223fa2d047d2c0b67ce92ab7bbe31fcd11504b Mon Sep 17 00:00:00 2001 From: Andrei Borza Date: Tue, 29 Oct 2024 13:18:52 +0100 Subject: [PATCH 11/20] Rewrite plugin to add auto-instrument option --- .../solidstart-spa/app.config.ts | 14 +- .../solidstart-spa/package.json | 5 +- ...rument.server.mjs => instrument.server.ts} | 0 .../solidstart-spa/src/middleware.ts | 6 + .../solidstart/app.config.ts | 21 +-- .../test-applications/solidstart/package.json | 5 +- ...rument.server.mjs => instrument.server.ts} | 0 .../solidstart/src/middleware.ts | 6 + .../src/config/addInstrumentation.ts | 122 ++++++++++++------ packages/solidstart/src/config/index.ts | 1 - packages/solidstart/src/config/types.ts | 29 +---- packages/solidstart/src/config/utils.ts | 56 ++++++++ packages/solidstart/src/config/withSentry.ts | 36 ++---- packages/solidstart/src/vite/types.ts | 26 +++- .../test/config/addInstrumentation.test.ts | 67 +++------- .../solidstart/test/config/withSentry.test.ts | 38 ++---- 16 files changed, 231 insertions(+), 201 deletions(-) rename dev-packages/e2e-tests/test-applications/solidstart-spa/src/{instrument.server.mjs => instrument.server.ts} (100%) create mode 100644 dev-packages/e2e-tests/test-applications/solidstart-spa/src/middleware.ts rename dev-packages/e2e-tests/test-applications/solidstart/src/{instrument.server.mjs => instrument.server.ts} (100%) create mode 100644 dev-packages/e2e-tests/test-applications/solidstart/src/middleware.ts create mode 100644 packages/solidstart/src/config/utils.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts b/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts index d329d6066fc7..103ecb09a469 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/app.config.ts @@ -1,9 +1,9 @@ -import { sentrySolidStartVite } from '@sentry/solidstart'; +import { withSentry } from '@sentry/solidstart'; import { defineConfig } from '@solidjs/start/config'; -export default defineConfig({ - ssr: false, - vite: { - plugins: [sentrySolidStartVite()], - }, -}); +export default defineConfig( + withSentry({ + ssr: false, + middleware: './src/middleware.ts', + }), +); diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json index f4ff0802e159..da6d33ddfc80 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -3,9 +3,8 @@ "version": "0.0.0", "scripts": { "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", - "dev": "NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev", - "build": "vinxi build && sh ./post_build.sh", - "preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", + "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" diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.mjs rename to dev-packages/e2e-tests/test-applications/solidstart-spa/src/instrument.server.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/src/middleware.ts b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/middleware.ts new file mode 100644 index 000000000000..88123a035fb6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/src/middleware.ts @@ -0,0 +1,6 @@ +import { sentryBeforeResponseMiddleware } from '@sentry/solidstart'; +import { createMiddleware } from '@solidjs/start/middleware'; + +export default createMiddleware({ + onBeforeResponse: [sentryBeforeResponseMiddleware()], +}); 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 337068467558..71061cf25d96 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts +++ b/dev-packages/e2e-tests/test-applications/solidstart/app.config.ts @@ -2,22 +2,7 @@ import { withSentry } from '@sentry/solidstart'; import { defineConfig } from '@solidjs/start/config'; 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', - }, - ), + withSentry({ + middleware: './src/middleware.ts', + }), ); diff --git a/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index 032a4af9058a..bf8a73e5ae10 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -3,9 +3,8 @@ "version": "0.0.0", "scripts": { "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", - "dev": "NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi dev", - "build": "vinxi build && sh ./post_build.sh", - "preview": "HOST=localhost PORT=3030 NODE_OPTIONS='--import ./src/instrument.server.mjs' vinxi start", + "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" diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.mjs b/dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts similarity index 100% rename from dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.mjs rename to dev-packages/e2e-tests/test-applications/solidstart/src/instrument.server.ts diff --git a/dev-packages/e2e-tests/test-applications/solidstart/src/middleware.ts b/dev-packages/e2e-tests/test-applications/solidstart/src/middleware.ts new file mode 100644 index 000000000000..88123a035fb6 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart/src/middleware.ts @@ -0,0 +1,6 @@ +import { sentryBeforeResponseMiddleware } from '@sentry/solidstart'; +import { createMiddleware } from '@solidjs/start/middleware'; + +export default createMiddleware({ + onBeforeResponse: [sentryBeforeResponseMiddleware()], +}); diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts index 1aab37cfc157..8fdd056e9adf 100644 --- a/packages/solidstart/src/config/addInstrumentation.ts +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -1,7 +1,16 @@ import * as fs from 'fs'; import * as path from 'path'; -import { consoleSandbox } from '@sentry/utils'; -import type { Nitro } from './types'; +import { consoleSandbox, flatten } from '@sentry/utils'; +import type { Nitro } from 'nitropack'; +import type { InputPluginOption } from 'rollup'; +import type { RollupConfig } from './types'; +import { + QUERY_END_INDICATOR, + SENTRY_FUNCTIONS_REEXPORT, + SENTRY_WRAPPED_ENTRY, + constructFunctionReExport, + removeSentryQueryFromPath, +} from './utils'; // Nitro presets for hosts that only host static files export const staticHostPresets = ['github_pages']; @@ -42,54 +51,81 @@ export async function addInstrumentationFileToBuild(nitro: Nitro): Promise } /** - * 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 { +export async function addAutoInstrumentation(nitro: Nitro, config: RollupConfig): Promise { // Static file hosts have no server component so there's nothing to do - if (staticHostPresets.includes(preset)) { + if (staticHostPresets.includes(nitro.options.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); + const buildDir = nitro.options.buildDir; + const serverInstrumentationPath = path.resolve(buildDir, 'build', 'ssr', 'instrument.server.js'); - 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; - } + config.plugins.push({ + name: 'sentry-solidstart-auto-instrument', + async resolveId(source, importer, options) { + if (source.includes('instrument.server.js')) { + return { id: source, moduleSideEffects: true }; + } - try { - const content = await fs.promises.readFile(serverEntryFile, 'utf-8'); - const updatedContent = `import './instrument.server.mjs';\n${content}`; - await fs.promises.writeFile(serverEntryFile, updatedContent); + if (source === 'import-in-the-middle/hook.mjs') { + // We are importing "import-in-the-middle" 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 }; + } - 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, - ); - } + if (options.isEntry && 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?.external) return resolution; + + const moduleInfo = await this.load(resolution); + + moduleInfo.moduleSideEffects = true; + + // The key `.` in `exportedBindings` refer to the exports within the file + const functionsToExport = flatten(Object.values(moduleInfo.exportedBindings || {})).filter(functionName => + ['default', 'handler', 'server'].includes(functionName), + ); + + // 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 + : 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(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '') + .concat(QUERY_END_INDICATOR); + } + + return null; + }, + load(id: string) { + if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { + const entryId = removeSentryQueryFromPath(id); + + // Mostly useful for serverless `handler` functions + const reExportedFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT) + ? constructFunctionReExport(id, entryId) + : ''; + + return [ + // Regular `import` of the Sentry config + `import ${JSON.stringify(serverInstrumentationPath)};`, + // 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)});`, + // By importing "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()`. + "import 'import-in-the-middle/hook.mjs';", + `${reExportedFunctions}`, + ].join('\n'); + } + + return null; + }, + } satisfies InputPluginOption); } diff --git a/packages/solidstart/src/config/index.ts b/packages/solidstart/src/config/index.ts index 4cf4b985c18a..4949f4bdf523 100644 --- a/packages/solidstart/src/config/index.ts +++ b/packages/solidstart/src/config/index.ts @@ -1,2 +1 @@ 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 index 2c67942c8a4d..0d6ea9bdf4f4 100644 --- a/packages/solidstart/src/config/types.ts +++ b/packages/solidstart/src/config/types.ts @@ -1,14 +1,9 @@ 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; - }; +import type { Nitro } from 'nitropack'; + +// Nitro does not export this type +export type RollupConfig = { + plugins: unknown[]; }; export type SolidStartInlineConfig = Parameters[0]; @@ -19,17 +14,3 @@ export type SolidStartInlineServerConfig = { '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/utils.ts b/packages/solidstart/src/config/utils.ts new file mode 100644 index 000000000000..ce6e0f6ea8e2 --- /dev/null +++ b/packages/solidstart/src/config/utils.ts @@ -0,0 +1,56 @@ +export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry'; +export const SENTRY_FUNCTIONS_REEXPORT = '?sentry-query-functions-reexport='; +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 query parameters from a query string. + * If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting + * serverless `handler` functions. + * + * Only exported for testing. + */ +export function extractFunctionReexportQueryParameters(query: string): string[] { + // Regex matches the comma-separated params between the functions query + // eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor + const regex = new RegExp(`\\${SENTRY_FUNCTIONS_REEXPORT}(.*?)\\${QUERY_END_INDICATOR}`); + const match = query.match(regex); + + return match && match[1] + ? match[1] + .split(',') + .filter(param => param !== '') + // Sanitize, as code could be injected with another rollup plugin + .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) + : []; +} + +/** + * Constructs a code snippet with function reexports (can be used in Rollup plugins) + */ +export function constructFunctionReExport(pathWithQuery: string, entryId: string): string { + const functionNames = extractFunctionReexportQueryParameters(pathWithQuery); + + return functionNames.reduce( + (functionsCode, currFunctionName) => + functionsCode.concat( + 'async function reExport(...args) {\n' + + ` const res = await import(${JSON.stringify(entryId)});\n` + + ` return res.${currFunctionName}.call(this, ...args);\n` + + '}\n' + + `export { reExport as ${currFunctionName} };\n`, + ), + '', + ); +} diff --git a/packages/solidstart/src/config/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index a91d7e4458ad..70ed8d872f80 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -1,10 +1,12 @@ +import type { Nitro } from 'nitropack'; import { addSentryPluginToVite } from '../vite'; import type { SentrySolidStartPluginOptions } from '../vite/types'; -import { - addInstrumentationFileToBuild, - experimental_addInstrumentationFileTopLevelImportToServerEntry, -} from './addInstrumentation'; -import type { Nitro, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types'; +import { addAutoInstrumentation, addInstrumentationFileToBuild } from './addInstrumentation'; +import type { RollupConfig, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types'; + +const defaultSentrySolidStartPluginOptions: SentrySolidStartPluginOptions = { + autoInstrument: true, +}; /** * Modifies the passed in Solid Start configuration with build-time enhancements such as @@ -17,7 +19,7 @@ import type { Nitro, SolidStartInlineConfig, SolidStartInlineServerConfig } from */ export const withSentry = ( solidStartConfig: SolidStartInlineConfig = {}, - sentrySolidStartPluginOptions: SentrySolidStartPluginOptions = {}, + sentrySolidStartPluginOptions: SentrySolidStartPluginOptions = defaultSentrySolidStartPluginOptions, ): SolidStartInlineConfig => { const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig; const hooks = server.hooks || {}; @@ -26,9 +28,6 @@ export const withSentry = ( ? (...args: unknown[]) => addSentryPluginToVite(solidStartConfig.vite(...args), sentrySolidStartPluginOptions) : addSentryPluginToVite(solidStartConfig.vite, sentrySolidStartPluginOptions); - let serverDir: string; - let buildPreset: string; - return { ...solidStartConfig, vite, @@ -36,21 +35,12 @@ export const withSentry = ( ...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, config: RollupConfig) { + if (sentrySolidStartPluginOptions.autoInstrument) { + await addAutoInstrumentation(nitro, config); + } else { + await addInstrumentationFileToBuild(nitro); } - }, - 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']) { diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index c31e901efc2e..79b4edef1501 100644 --- a/packages/solidstart/src/vite/types.ts +++ b/packages/solidstart/src/vite/types.ts @@ -135,14 +135,28 @@ export type SentrySolidStartPluginOptions = { 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.). + * When `true`, automatically bundles the instrumentation file into + * the nitro server entry file and dynamically imports (`import()`) the original server + * entry file so that Sentry can instrument the server side of the application. * - * 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. + * When `false`, the Sentry instrument file is added as a separate file to the + * nitro server output directory alongside the server entry file. To instrument the + * server side of the application, add + * `--import ./.output/server/instrument.server.mjs` to your `NODE_OPTIONS`. * - * **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: true + */ + autoInstrument?: boolean; + + /** + * By default (unless you configure `autoInstrument: false`), the SDK will try to wrap your + * application entrypoint with a dynamic `import()` to ensure all dependencies can be properly instrumented. + * + * By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint. + * If your application has a different main export that is used to run the application, you can overwrite this by + * providing an array of export names to wrap. * - * @default false + * Any wrapped export is expected to be an async function. */ - experimental_basicServerTracing?: boolean; + asyncFunctionsToReExport?: string[]; }; diff --git a/packages/solidstart/test/config/addInstrumentation.test.ts b/packages/solidstart/test/config/addInstrumentation.test.ts index 7f20911a70b3..45b44b853de6 100644 --- a/packages/solidstart/test/config/addInstrumentation.test.ts +++ b/packages/solidstart/test/config/addInstrumentation.test.ts @@ -1,9 +1,9 @@ +import type { RollupConfig } from 'vite'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import type { Nitro } from '../../build/types/config/types'; import { + addAutoInstrumentation, addInstrumentationFileToBuild, - experimental_addInstrumentationFileTopLevelImportToServerEntry, - serverFilePresets, staticHostPresets, } from '../../src/config/addInstrumentation'; @@ -82,54 +82,23 @@ describe('addInstrumentationFileToBuild()', () => { }); }); -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`.', - ); +describe('addAutoInstrumentation()', () => { + const nitroOptions: Nitro = { + options: { + buildDir: '/path/to/buildDir', + output: { + serverDir: '/path/to/serverDir', + }, + preset: 'vercel', }, - ); + }; - 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('adds the `sentry-solidstart-auto-instrument` rollup plugin to the rollup config', async () => { + const rollupConfig: RollupConfig = { + plugins: [], + }; - 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(); - }, - ); + await addAutoInstrumentation(nitroOptions, rollupConfig); + expect(rollupConfig.plugins.find(plugin => plugin.name === 'sentry-solidstart-auto-instrument')).toBeTruthy(); + }); }); diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts index 52ebb2449c25..afb22a10664f 100644 --- a/packages/solidstart/test/config/withSentry.test.ts +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -1,17 +1,17 @@ +import type { Nitro } from 'nitropack'; import type { Plugin } from 'vite'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Nitro } from '../../src/config'; +import type { RollupConfig } from '../../build/types/config/types'; import { withSentry } from '../../src/config'; const userDefinedNitroRollupBeforeHookMock = vi.fn(); const userDefinedNitroCloseHookMock = vi.fn(); const addInstrumentationFileToBuildMock = vi.fn(); -const experimental_addInstrumentationFileTopLevelImportToServerEntryMock = vi.fn(); +const addAutoInstrumentationMock = vi.fn(); vi.mock('../../src/config/addInstrumentation', () => ({ addInstrumentationFileToBuild: (...args: unknown[]) => addInstrumentationFileToBuildMock(...args), - experimental_addInstrumentationFileTopLevelImportToServerEntry: (...args: unknown[]) => - experimental_addInstrumentationFileTopLevelImportToServerEntryMock(...args), + addAutoInstrumentation: (...args: unknown[]) => addAutoInstrumentationMock(...args), })); beforeEach(() => { @@ -37,31 +37,21 @@ describe('withSentry()', () => { preset: 'vercel', }, }; + const rollupConfig: RollupConfig = { + plugins: [], + }; - it('adds a nitro hook to add the instrumentation file to the build', async () => { + it('adds a nitro hook to auto instrumentation the backend', 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(); + await config?.server.hooks['rollup:before'](nitroOptions, rollupConfig); + expect(addAutoInstrumentationMock).toHaveBeenCalledWith(nitroOptions, rollupConfig); }); - 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 }); + it('adds a nitro hook to add the instrumentation file to the build if auto instrumentation is turned off', async () => { + const config = withSentry(solidStartConfig, { autoInstrument: false }); await config?.server.hooks['rollup:before'](nitroOptions); - await config?.server.hooks['close'](nitroOptions); - expect(experimental_addInstrumentationFileTopLevelImportToServerEntryMock).not.toHaveBeenCalled(); - expect(userDefinedNitroCloseHookMock).toHaveBeenCalled(); + expect(addInstrumentationFileToBuildMock).toHaveBeenCalledWith(nitroOptions); + expect(userDefinedNitroRollupBeforeHookMock).toHaveBeenCalledWith(nitroOptions); }); it('adds the sentry solidstart vite plugin', () => { From 9c1d06d0163831015b2b8e6fc205a40aac49ef1c Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 30 Dec 2024 15:10:14 +0100 Subject: [PATCH 12/20] feat(solidstart)!: Default to `--import` setup and add `autoInjectServerSentry` --- .../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 | 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-top-level-import/tsconfig.json | 19 ++ .../vitest.config.ts | 10 + .../test-applications/solidstart/package.json | 1 + .../solidstart/playwright.config.mjs | 2 +- packages/solidstart/README.md | 4 +- .../src/config/addInstrumentation.ts | 215 +++++++++--------- packages/solidstart/src/config/utils.ts | 82 ++++--- packages/solidstart/src/config/withSentry.ts | 34 +-- .../src/vite/buildInstrumentationFile.ts | 2 +- packages/solidstart/src/vite/types.ts | 30 +-- .../test/config/addInstrumentation.test.ts | 30 +-- .../solidstart/test/config/withSentry.test.ts | 44 ++-- 36 files changed, 950 insertions(+), 215 deletions(-) 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 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..f5a5494a1a90 --- /dev/null +++ b/dev-packages/e2e-tests/test-applications/solidstart-top-level-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-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'); + // 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-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/dev-packages/e2e-tests/test-applications/solidstart/package.json b/dev-packages/e2e-tests/test-applications/solidstart/package.json index bf8a73e5ae10..020bedb41806 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart/package.json @@ -5,6 +5,7 @@ "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", "build": "vinxi build && sh post_build.sh", "preview": "HOST=localhost PORT=3030 vinxi start", + "start:import": "HOST=localhost PORT=3030 node --import ./.output/server/instrument.server.mjs .output/server/index.mjs", "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" diff --git a/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs index 395acfc282f9..ee2ee42980b8 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart/playwright.config.mjs @@ -1,7 +1,7 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: 'pnpm preview', + startCommand: 'pnpm start:import', port: 3030, }); diff --git a/packages/solidstart/README.md b/packages/solidstart/README.md index 0a410384bae0..28127c336c0d 100644 --- a/packages/solidstart/README.md +++ b/packages/solidstart/README.md @@ -159,7 +159,7 @@ where `instrument.server.mjs` is located, monitor the build log output for 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 +For such platforms, we offer the option `autoInjectServerSentry: 'top-level-import'` to add a top level import of `instrument.server.mjs` to the server entry file. ```typescript @@ -180,7 +180,7 @@ export default defineConfig( // 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, + autoInjectServerSentry: 'top-level-import', }, ), ); diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts index 8fdd056e9adf..ceb70cf118d6 100644 --- a/packages/solidstart/src/config/addInstrumentation.ts +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -1,16 +1,7 @@ import * as fs from 'fs'; import * as path from 'path'; -import { consoleSandbox, flatten } from '@sentry/utils'; +import { consoleSandbox } from '@sentry/core'; import type { Nitro } from 'nitropack'; -import type { InputPluginOption } from 'rollup'; -import type { RollupConfig } from './types'; -import { - QUERY_END_INDICATOR, - SENTRY_FUNCTIONS_REEXPORT, - SENTRY_WRAPPED_ENTRY, - constructFunctionReExport, - removeSentryQueryFromPath, -} from './utils'; // Nitro presets for hosts that only host static files export const staticHostPresets = ['github_pages']; @@ -20,112 +11,126 @@ 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. 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'); + 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}.`); + }); + } - try { - await fs.promises.copyFile(source, destination); + // 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); + }); + } - 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); - }); - } + // 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] Build process failed.', 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 addAutoInstrumentation(nitro: Nitro, config: RollupConfig): 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 serverInstrumentationPath = path.resolve(buildDir, 'build', 'ssr', 'instrument.server.js'); - - config.plugins.push({ - name: 'sentry-solidstart-auto-instrument', - async resolveId(source, importer, options) { - if (source.includes('instrument.server.js')) { - return { id: source, moduleSideEffects: true }; - } - - if (source === 'import-in-the-middle/hook.mjs') { - // We are importing "import-in-the-middle" 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('.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?.external) return resolution; - - const moduleInfo = await this.load(resolution); - - moduleInfo.moduleSideEffects = true; - - // The key `.` in `exportedBindings` refer to the exports within the file - const functionsToExport = flatten(Object.values(moduleInfo.exportedBindings || {})).filter(functionName => - ['default', 'handler', 'server'].includes(functionName), +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, ); - - // 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 - : 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(functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '') - .concat(QUERY_END_INDICATOR); - } - - return null; - }, - load(id: string) { - if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) { - const entryId = removeSentryQueryFromPath(id); - - // Mostly useful for serverless `handler` functions - const reExportedFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT) - ? constructFunctionReExport(id, entryId) - : ''; - - return [ - // Regular `import` of the Sentry config - `import ${JSON.stringify(serverInstrumentationPath)};`, - // 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)});`, - // By importing "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()`. - "import 'import-in-the-middle/hook.mjs';", - `${reExportedFunctions}`, - ].join('\n'); - } - - return null; - }, - } satisfies InputPluginOption); + }); + 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/utils.ts b/packages/solidstart/src/config/utils.ts index ce6e0f6ea8e2..fd4b70d508d0 100644 --- a/packages/solidstart/src/config/utils.ts +++ b/packages/solidstart/src/config/utils.ts @@ -1,5 +1,6 @@ export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry'; -export const SENTRY_FUNCTIONS_REEXPORT = '?sentry-query-functions-reexport='; +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'; /** @@ -15,42 +16,67 @@ export function removeSentryQueryFromPath(url: string): string { } /** - * Extracts and sanitizes function re-export query parameters from a query string. - * If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting - * serverless `handler` functions. + * 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): string[] { +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 regex = new RegExp(`\\${SENTRY_FUNCTIONS_REEXPORT}(.*?)\\${QUERY_END_INDICATOR}`); - const match = query.match(regex); - - return match && match[1] - ? match[1] - .split(',') - .filter(param => param !== '') - // Sanitize, as code could be injected with another rollup plugin - .map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')) - : []; + 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) + * 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 functionNames = extractFunctionReexportQueryParameters(pathWithQuery); - - return functionNames.reduce( - (functionsCode, currFunctionName) => - functionsCode.concat( - 'async function reExport(...args) {\n' + - ` const res = await import(${JSON.stringify(entryId)});\n` + - ` return res.${currFunctionName}.call(this, ...args);\n` + - '}\n' + - `export { reExport as ${currFunctionName} };\n`, + 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 index 70ed8d872f80..65d9f5100716 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -1,12 +1,8 @@ import type { Nitro } from 'nitropack'; import { addSentryPluginToVite } from '../vite'; import type { SentrySolidStartPluginOptions } from '../vite/types'; -import { addAutoInstrumentation, addInstrumentationFileToBuild } from './addInstrumentation'; -import type { RollupConfig, SolidStartInlineConfig, SolidStartInlineServerConfig } from './types'; - -const defaultSentrySolidStartPluginOptions: SentrySolidStartPluginOptions = { - autoInstrument: true, -}; +import { addInstrumentationFileToBuild, addSentryTopImport } from './addInstrumentation'; +import type { SolidStartInlineConfig, SolidStartInlineServerConfig } from './types'; /** * Modifies the passed in Solid Start configuration with build-time enhancements such as @@ -17,16 +13,20 @@ const defaultSentrySolidStartPluginOptions: SentrySolidStartPluginOptions = { * @param sentrySolidStartPluginOptions Options to configure the plugin * @returns The modified config to be exported and passed back into `defineConfig` */ -export const withSentry = ( +export function withSentry( solidStartConfig: SolidStartInlineConfig = {}, - sentrySolidStartPluginOptions: SentrySolidStartPluginOptions = defaultSentrySolidStartPluginOptions, -): SolidStartInlineConfig => { + sentrySolidStartPluginOptions: SentrySolidStartPluginOptions, +): SolidStartInlineConfig { + const sentryPluginOptions = { + ...sentrySolidStartPluginOptions, + }; + 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); + ? (...args: unknown[]) => addSentryPluginToVite(solidStartConfig.vite(...args), sentryPluginOptions) + : addSentryPluginToVite(solidStartConfig.vite, sentryPluginOptions); return { ...solidStartConfig, @@ -35,11 +35,11 @@ export const withSentry = ( ...server, hooks: { ...hooks, - async 'rollup:before'(nitro: Nitro, config: RollupConfig) { - if (sentrySolidStartPluginOptions.autoInstrument) { - await addAutoInstrumentation(nitro, config); - } else { - await addInstrumentationFileToBuild(nitro); + async 'rollup:before'(nitro: Nitro) { + await addInstrumentationFileToBuild(nitro); + + if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'top-level-import') { + await addSentryTopImport(nitro); } // Run user provided hook @@ -50,4 +50,4 @@ export const withSentry = ( }, }, }; -}; +} diff --git a/packages/solidstart/src/vite/buildInstrumentationFile.ts b/packages/solidstart/src/vite/buildInstrumentationFile.ts index abb02e8d03ce..81bcef7a5bf7 100644 --- a/packages/solidstart/src/vite/buildInstrumentationFile.ts +++ b/packages/solidstart/src/vite/buildInstrumentationFile.ts @@ -1,6 +1,6 @@ import * as fs from 'fs'; import * as path from 'path'; -import { consoleSandbox } from '@sentry/utils'; +import { consoleSandbox } from '@sentry/core'; import type { Plugin, UserConfig } from 'vite'; import type { SentrySolidStartPluginOptions } from './types'; diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index 79b4edef1501..5f34f0c4b2d8 100644 --- a/packages/solidstart/src/vite/types.ts +++ b/packages/solidstart/src/vite/types.ts @@ -135,28 +135,20 @@ export type SentrySolidStartPluginOptions = { instrumentation?: string; /** - * When `true`, automatically bundles the instrumentation file into - * the nitro server entry file and dynamically imports (`import()`) the original server - * entry file so that Sentry can instrument the server side of the application. * - * When `false`, the Sentry instrument file is added as a separate file to the - * nitro server output directory alongside the server entry file. To instrument the - * server side of the application, add - * `--import ./.output/server/instrument.server.mjs` to your `NODE_OPTIONS`. + * Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible. * - * @default: true - */ - autoInstrument?: boolean; - - /** - * By default (unless you configure `autoInstrument: false`), the SDK will try to wrap your - * application entrypoint with a dynamic `import()` to ensure all dependencies can be properly instrumented. + * **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"** * - * By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint. - * If your application has a different main export that is used to run the application, you can overwrite this by - * providing an array of export names to wrap. + * 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.). * - * Any wrapped export is expected to be an async function. + * 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. */ - asyncFunctionsToReExport?: string[]; + autoInjectServerSentry?: 'top-level-import'; }; diff --git a/packages/solidstart/test/config/addInstrumentation.test.ts b/packages/solidstart/test/config/addInstrumentation.test.ts index 45b44b853de6..b1fc9eeae59a 100644 --- a/packages/solidstart/test/config/addInstrumentation.test.ts +++ b/packages/solidstart/test/config/addInstrumentation.test.ts @@ -1,11 +1,6 @@ -import type { RollupConfig } from 'vite'; +import type { Nitro } from 'nitropack'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { Nitro } from '../../build/types/config/types'; -import { - addAutoInstrumentation, - addInstrumentationFileToBuild, - staticHostPresets, -} from '../../src/config/addInstrumentation'; +import { addInstrumentationFileToBuild, staticHostPresets } from '../../src/config/addInstrumentation'; const consoleLogSpy = vi.spyOn(console, 'log'); const consoleWarnSpy = vi.spyOn(console, 'warn'); @@ -81,24 +76,3 @@ describe('addInstrumentationFileToBuild()', () => { expect(fsCopyFileMock).not.toHaveBeenCalled(); }); }); - -describe('addAutoInstrumentation()', () => { - const nitroOptions: Nitro = { - options: { - buildDir: '/path/to/buildDir', - output: { - serverDir: '/path/to/serverDir', - }, - preset: 'vercel', - }, - }; - - it('adds the `sentry-solidstart-auto-instrument` rollup plugin to the rollup config', async () => { - const rollupConfig: RollupConfig = { - plugins: [], - }; - - await addAutoInstrumentation(nitroOptions, rollupConfig); - expect(rollupConfig.plugins.find(plugin => plugin.name === 'sentry-solidstart-auto-instrument')).toBeTruthy(); - }); -}); diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts index afb22a10664f..4a822d12586d 100644 --- a/packages/solidstart/test/config/withSentry.test.ts +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -1,17 +1,16 @@ import type { Nitro } from 'nitropack'; import type { Plugin } from 'vite'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import type { RollupConfig } from '../../build/types/config/types'; import { withSentry } from '../../src/config'; const userDefinedNitroRollupBeforeHookMock = vi.fn(); const userDefinedNitroCloseHookMock = vi.fn(); const addInstrumentationFileToBuildMock = vi.fn(); -const addAutoInstrumentationMock = vi.fn(); +const addSentryTopImportMock = vi.fn(); vi.mock('../../src/config/addInstrumentation', () => ({ addInstrumentationFileToBuild: (...args: unknown[]) => addInstrumentationFileToBuildMock(...args), - addAutoInstrumentation: (...args: unknown[]) => addAutoInstrumentationMock(...args), + addSentryTopImport: (...args: unknown[]) => addSentryTopImportMock(...args), })); beforeEach(() => { @@ -37,23 +36,40 @@ describe('withSentry()', () => { preset: 'vercel', }, }; - const rollupConfig: RollupConfig = { - plugins: [], - }; - - it('adds a nitro hook to auto instrumentation the backend', async () => { - const config = withSentry(solidStartConfig); - await config?.server.hooks['rollup:before'](nitroOptions, rollupConfig); - expect(addAutoInstrumentationMock).toHaveBeenCalledWith(nitroOptions, rollupConfig); - }); - it('adds a nitro hook to add the instrumentation file to the build if auto instrumentation is turned off', async () => { - const config = withSentry(solidStartConfig, { autoInstrument: false }); + 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', From 9c8ba4b105e3bb904059d7fc933b86b77cb82cd6 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 2 Jan 2025 10:41:46 +0100 Subject: [PATCH 13/20] fix tests --- .../solidstart-spa/package.json | 1 + .../solidstart-spa/playwright.config.mjs | 2 +- .../src/config/addInstrumentation.ts | 2 +- .../test/config/addInstrumentation.test.ts | 127 ++++++++++++++++-- 4 files changed, 122 insertions(+), 10 deletions(-) diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json index da6d33ddfc80..e0e2a04d0bd4 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/package.json @@ -5,6 +5,7 @@ "clean": "pnpx rimraf node_modules pnpm-lock.yaml .vinxi .output", "build": "vinxi build && sh post_build.sh", "preview": "HOST=localhost PORT=3030 vinxi start", + "start:import": "HOST=localhost PORT=3030 node --import ./.output/server/instrument.server.mjs .output/server/index.mjs", "test:prod": "TEST_ENV=production playwright test", "test:build": "pnpm install && pnpm build", "test:assert": "pnpm test:prod" diff --git a/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs b/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs index 395acfc282f9..ee2ee42980b8 100644 --- a/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs +++ b/dev-packages/e2e-tests/test-applications/solidstart-spa/playwright.config.mjs @@ -1,7 +1,7 @@ import { getPlaywrightConfig } from '@sentry-internal/test-utils'; const config = getPlaywrightConfig({ - startCommand: 'pnpm preview', + startCommand: 'pnpm start:import', port: 3030, }); diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts index ceb70cf118d6..1d5288def5eb 100644 --- a/packages/solidstart/src/config/addInstrumentation.ts +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -74,7 +74,7 @@ export async function addInstrumentationFileToBuild(nitro: Nitro): Promise } catch (error) { consoleSandbox(() => { // eslint-disable-next-line no-console - console.warn('[Sentry SolidStart withSentry] Build process failed.', error); + console.warn('[Sentry SolidStart withSentry] Failed to add instrumentation file to build.', error); }); } }); diff --git a/packages/solidstart/test/config/addInstrumentation.test.ts b/packages/solidstart/test/config/addInstrumentation.test.ts index b1fc9eeae59a..cddbd4821e3f 100644 --- a/packages/solidstart/test/config/addInstrumentation.test.ts +++ b/packages/solidstart/test/config/addInstrumentation.test.ts @@ -8,11 +8,15 @@ 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, @@ -20,6 +24,8 @@ vi.mock('fs', async () => { 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), }, }; }); @@ -30,6 +36,9 @@ beforeEach(() => { describe('addInstrumentationFileToBuild()', () => { const nitroOptions: Nitro = { + hooks: { + hook: vi.fn(), + }, options: { buildDir: '/path/to/buildDir', output: { @@ -39,40 +48,142 @@ describe('addInstrumentationFileToBuild()', () => { }, }; + 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', ); - 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 () => { + 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 create /path/to/serverDir/instrument.server.mjs.', + '[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 => { - await addInstrumentationFileToBuild({ + 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, + ); + }); }); From 61cc30018594888b1c3d7596eeb3203823d72876 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 2 Jan 2025 11:01:18 +0100 Subject: [PATCH 14/20] add changelog entry --- CHANGELOG.md | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 210e2c13ea4b..78e8091c6057 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,42 @@ Work in this release was contributed by @aloisklink, @arturovt, @benjick and @maximepvrt. Thank you for your contributions! +- **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 Solid 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 Solid config options... */ + }, + { + // Options for setting up source maps + org: process.env.SENTRY_ORG, + project: process.env.SENTRY_PROJECT, + authToken: process.env.SENTRY_AUTH_TOKEN, + + // Optional: Install Sentry with a top-level import + autoInjectServerSentry: 'top-level-import', + }, + ), +); +``` + +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) + ## 8.45.0 - feat(core): Add `handled` option to `captureConsoleIntegration` ([#14664](https://github.com/getsentry/sentry-javascript/pull/14664)) From 4631cf3db8bcdd3d9cd19d1d068ea144abe714f7 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 7 Jan 2025 16:30:10 +0100 Subject: [PATCH 15/20] review suggestions --- CHANGELOG.md | 20 +++++++++++++------ .../solidstart/tests/errorboundary.test.ts | 2 -- .../src/config/addInstrumentation.ts | 3 +-- 3 files changed, 15 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 78e8091c6057..0a8f1a9538b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,7 +14,7 @@ Work in this release was contributed by @aloisklink, @arturovt, @benjick and @ma - **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 Solid Config with `withSentry`. The `sentrySolidStartVite` plugin is now automatically +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 @@ -24,16 +24,13 @@ import { withSentry } from '@sentry/solidstart'; export default defineConfig( withSentry( { - /* Your Solid config options... */ + /* 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, - - // Optional: Install Sentry with a top-level import - autoInjectServerSentry: 'top-level-import', }, ), ); @@ -44,9 +41,20 @@ Add the Sentry server config in `src/instrument.server.ts`. Then, the server con 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): +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.45.0 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 088f69df6380..b709760aab94 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,8 +11,6 @@ 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/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts index 1d5288def5eb..f0bca10ae3e3 100644 --- a/packages/solidstart/src/config/addInstrumentation.ts +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -15,8 +15,7 @@ export const serverFilePresets = ['netlify']; * 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. Make sure the `sentrySolidStartVite` plugin was - * added to `app.config.ts` to enable building the instrumentation file. + * build directory. */ export async function addInstrumentationFileToBuild(nitro: Nitro): Promise { nitro.hooks.hook('close', async () => { From 3d8bb0b558b2e22efb45b810ab49c8290d96fcaf Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 7 Jan 2025 16:37:25 +0100 Subject: [PATCH 16/20] move plugin --- packages/solidstart/src/vite/sentrySolidStartVite.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/packages/solidstart/src/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts index 227a303b0ad4..1bafc0cd07b5 100644 --- a/packages/solidstart/src/vite/sentrySolidStartVite.ts +++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts @@ -9,20 +9,14 @@ import type { SentrySolidStartPluginOptions } from './types'; export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}): Plugin[] => { const sentryPlugins: Plugin[] = []; + sentryPlugins.push(makeBuildInstrumentationFilePlugin(options)); + if (process.env.NODE_ENV !== 'development') { if (options.sourceMapsUploadOptions?.enabled ?? true) { sentryPlugins.push(...makeSourceMapsVitePlugin(options)); } } - // 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; }; From 76cc18bb9b99e5d7b8f30b6e88a44a73e26521db Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Tue, 7 Jan 2025 16:39:09 +0100 Subject: [PATCH 17/20] review suggestion --- .../solidstart-top-level-import/tests/errorboundary.test.ts | 2 -- 1 file changed, 2 deletions(-) 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 index f5a5494a1a90..49f50f882b50 100644 --- 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 @@ -11,8 +11,6 @@ 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; From 333d2105481e5ddb2cb2d787c1043eb441545712 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Wed, 8 Jan 2025 10:40:33 +0100 Subject: [PATCH 18/20] fix tests --- packages/solidstart/test/config/withSentry.test.ts | 6 +++--- packages/solidstart/test/vite/sentrySolidStartVite.test.ts | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/solidstart/test/config/withSentry.test.ts b/packages/solidstart/test/config/withSentry.test.ts index 4a822d12586d..e554db45124f 100644 --- a/packages/solidstart/test/config/withSentry.test.ts +++ b/packages/solidstart/test/config/withSentry.test.ts @@ -78,6 +78,7 @@ describe('withSentry()', () => { }); 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', @@ -85,7 +86,6 @@ describe('withSentry()', () => { 'sentry-vite-debug-id-injection-plugin', 'sentry-vite-debug-id-upload-plugin', 'sentry-file-deletion-plugin', - 'sentry-solidstart-build-instrumentation-file', ]); }); @@ -106,6 +106,7 @@ describe('withSentry()', () => { 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', @@ -113,7 +114,6 @@ describe('withSentry()', () => { 'sentry-vite-debug-id-injection-plugin', 'sentry-vite-debug-id-upload-plugin', 'sentry-file-deletion-plugin', - 'sentry-solidstart-build-instrumentation-file', 'my-test-plugin', ]); }); @@ -138,6 +138,7 @@ describe('withSentry()', () => { .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', @@ -145,7 +146,6 @@ describe('withSentry()', () => { '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/sentrySolidStartVite.test.ts b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts index 45faa8b797f9..8915c5a70671 100644 --- a/packages/solidstart/test/vite/sentrySolidStartVite.test.ts +++ b/packages/solidstart/test/vite/sentrySolidStartVite.test.ts @@ -23,6 +23,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', @@ -30,7 +31,6 @@ describe('sentrySolidStartVite()', () => { 'sentry-vite-debug-id-injection-plugin', 'sentry-vite-debug-id-upload-plugin', 'sentry-file-deletion-plugin', - 'sentry-solidstart-build-instrumentation-file', ]); }); From f8a9ab96c29a5fed8dbe4cf991447024249fa8c5 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Mon, 30 Dec 2024 15:40:45 +0100 Subject: [PATCH 19/20] feat(solidstart): Add `autoInjectServerSentry: 'experimental_dynamic-import'` --- .../src/config/addInstrumentation.ts | 47 ++++ packages/solidstart/src/config/withSentry.ts | 35 ++- .../wrapServerEntryWithDynamicImport.ts | 245 ++++++++++++++++++ .../src/vite/sentrySolidStartVite.ts | 4 +- packages/solidstart/src/vite/types.ts | 31 ++- .../test/config/addInstrumentation.test.ts | 35 ++- 6 files changed, 388 insertions(+), 9 deletions(-) create mode 100644 packages/solidstart/src/config/wrapServerEntryWithDynamicImport.ts diff --git a/packages/solidstart/src/config/addInstrumentation.ts b/packages/solidstart/src/config/addInstrumentation.ts index f0bca10ae3e3..74b72a12b4de 100644 --- a/packages/solidstart/src/config/addInstrumentation.ts +++ b/packages/solidstart/src/config/addInstrumentation.ts @@ -2,6 +2,9 @@ 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']; @@ -133,3 +136,47 @@ export async function addSentryTopImport(nitro: Nitro): Promise { } }); } + +/** + * 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/withSentry.ts b/packages/solidstart/src/config/withSentry.ts index 65d9f5100716..c1050f0da1cc 100644 --- a/packages/solidstart/src/config/withSentry.ts +++ b/packages/solidstart/src/config/withSentry.ts @@ -1,8 +1,21 @@ +import { logger } from '@sentry/core'; import type { Nitro } from 'nitropack'; import { addSentryPluginToVite } from '../vite'; import type { SentrySolidStartPluginOptions } from '../vite/types'; -import { addInstrumentationFileToBuild, addSentryTopImport } from './addInstrumentation'; -import type { SolidStartInlineConfig, SolidStartInlineServerConfig } from './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 @@ -19,6 +32,7 @@ export function withSentry( ): SolidStartInlineConfig { const sentryPluginOptions = { ...sentrySolidStartPluginOptions, + ...defaultSentrySolidStartPluginOptions, }; const server = (solidStartConfig.server || {}) as SolidStartInlineServerConfig; @@ -35,11 +49,20 @@ export function withSentry( ...server, hooks: { ...hooks, - async 'rollup:before'(nitro: Nitro) { - await addInstrumentationFileToBuild(nitro); + 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); + if (sentrySolidStartPluginOptions?.autoInjectServerSentry === 'top-level-import') { + await addSentryTopImport(nitro); + } } // Run user provided hook 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/vite/sentrySolidStartVite.ts b/packages/solidstart/src/vite/sentrySolidStartVite.ts index 1bafc0cd07b5..96805f1a8c65 100644 --- a/packages/solidstart/src/vite/sentrySolidStartVite.ts +++ b/packages/solidstart/src/vite/sentrySolidStartVite.ts @@ -9,7 +9,9 @@ import type { SentrySolidStartPluginOptions } from './types'; export const sentrySolidStartVite = (options: SentrySolidStartPluginOptions = {}): Plugin[] => { const sentryPlugins: Plugin[] = []; - sentryPlugins.push(makeBuildInstrumentationFilePlugin(options)); + if (options.autoInjectServerSentry !== 'experimental_dynamic-import') { + sentryPlugins.push(makeBuildInstrumentationFilePlugin(options)); + } if (process.env.NODE_ENV !== 'development') { if (options.sourceMapsUploadOptions?.enabled ?? true) { diff --git a/packages/solidstart/src/vite/types.ts b/packages/solidstart/src/vite/types.ts index 5f34f0c4b2d8..1ae73777c6a4 100644 --- a/packages/solidstart/src/vite/types.ts +++ b/packages/solidstart/src/vite/types.ts @@ -134,6 +134,12 @@ export type SentrySolidStartPluginOptions = { */ 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. @@ -149,6 +155,29 @@ export type SentrySolidStartPluginOptions = { * 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'] */ - autoInjectServerSentry?: 'top-level-import'; + experimental_entrypointWrappedFunctions?: string[]; }; diff --git a/packages/solidstart/test/config/addInstrumentation.test.ts b/packages/solidstart/test/config/addInstrumentation.test.ts index cddbd4821e3f..012bca76c9ca 100644 --- a/packages/solidstart/test/config/addInstrumentation.test.ts +++ b/packages/solidstart/test/config/addInstrumentation.test.ts @@ -1,6 +1,11 @@ import type { Nitro } from 'nitropack'; import { beforeEach, describe, expect, it, vi } from 'vitest'; -import { addInstrumentationFileToBuild, staticHostPresets } from '../../src/config/addInstrumentation'; +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'); @@ -187,3 +192,31 @@ describe('addInstrumentationFileToBuild()', () => { ); }); }); + +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(); + }); +}); From f84bcf5cdc0bcab0e363e4dff323e988290ca7d7 Mon Sep 17 00:00:00 2001 From: s1gr1d Date: Thu, 2 Jan 2025 11:23:52 +0100 Subject: [PATCH 20/20] add E2E test --- .../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 ++ 26 files changed, 721 insertions(+) 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 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_', +});