diff --git a/packages/nuxt/src/common/types.ts b/packages/nuxt/src/common/types.ts index 96d69354937c..7c84775e6820 100644 --- a/packages/nuxt/src/common/types.ts +++ b/packages/nuxt/src/common/types.ts @@ -222,6 +222,24 @@ export type SentryNuxtModuleOptions = BuildTimeOptionsBase & { */ autoInjectServerSentry?: 'top-level-import' | 'experimental_dynamic-import'; + /** + * Provide the resolved path to a custom Sentry client config file. + * + * If not provided, the default location (`/sentry.(client|server).config.(js|ts)`) will be used to look up the config file. + * If there is no file at the default location either, the SDK won't be initialized. + * + * Resolves the full path to a file or directory, respecting Nuxt alias and extensions options. + * @example + * + * ```ts + * sentry: { + * configDir: '~/sentry-config', + * // Sentry will search for `//sentry-config/sentry.(client|server).config.(js|ts)` files. + * } + * ``` + */ + configDir?: string; + /** * 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. diff --git a/packages/nuxt/src/module.ts b/packages/nuxt/src/module.ts index bccba280b9ce..72ab409ba384 100644 --- a/packages/nuxt/src/module.ts +++ b/packages/nuxt/src/module.ts @@ -47,7 +47,7 @@ export default defineNuxtModule({ const moduleDirResolver = createResolver(import.meta.url); const buildDirResolver = createResolver(nuxt.options.buildDir); - const clientConfigFile = findDefaultSdkInitFile('client', nuxt); + const clientConfigFile = await findDefaultSdkInitFile('client', nuxt, moduleOptions); if (clientConfigFile) { // Inject the client-side Sentry config file with a side effect import @@ -78,7 +78,7 @@ export default defineNuxtModule({ }); } - const serverConfigFile = findDefaultSdkInitFile('server', nuxt); + const serverConfigFile = await findDefaultSdkInitFile('server', nuxt, moduleOptions); const isNitroV3 = (await getNitroMajorVersion()) >= 3; const nuxtMajor = parseInt((nuxt as unknown as { _version: string })._version?.split('.')[0] ?? '3', 10); const isMinNuxtV4 = nuxtMajor >= 4; diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index 86eafaae2c9b..58288302ae0d 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -2,6 +2,8 @@ import type { Nuxt } from '@nuxt/schema'; import { consoleSandbox } from '@sentry/core'; import * as fs from 'fs'; import * as path from 'path'; +import type { SentryNuxtModuleOptions } from '../common/types'; +import { resolvePath } from '@nuxt/kit'; /** * Gets the major version of the installed nitro package. @@ -25,7 +27,11 @@ export async function getNitroMajorVersion(): Promise { * Find the default SDK init file for the given type (client or server). * The sentry.server.config file is prioritized over the instrument.server file. */ -export function findDefaultSdkInitFile(type: 'server' | 'client', nuxt?: Nuxt): string | undefined { +export async function findDefaultSdkInitFile( + type: 'server' | 'client', + nuxt?: Nuxt, + options?: SentryNuxtModuleOptions, +): Promise { const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts']; const relativePaths: string[] = []; @@ -53,9 +59,9 @@ export function findDefaultSdkInitFile(type: 'server' | 'client', nuxt?: Nuxt): } // As a fallback, also check CWD (left for pure compatibility) - const cwd = process.cwd(); + const rootDir = options?.configDir ? await resolvePath(options.configDir, { type: 'dir' }) : process.cwd(); for (const relativePath of relativePaths) { - const fullPath = path.resolve(cwd, relativePath); + const fullPath = path.resolve(rootDir, relativePath); if (fs.existsSync(fullPath)) { return fullPath; } diff --git a/packages/nuxt/test/vite/buildOptions.test-d.ts b/packages/nuxt/test/vite/buildOptions.test-d.ts index ac2adde02ece..1ad1560e54ec 100644 --- a/packages/nuxt/test/vite/buildOptions.test-d.ts +++ b/packages/nuxt/test/vite/buildOptions.test-d.ts @@ -55,6 +55,7 @@ describe('Sentry Nuxt build-time options type', () => { // --- SentryNuxtModuleOptions specific options --- enabled: true, autoInjectServerSentry: 'experimental_dynamic-import', + configDir: '~/custom-config', experimental_entrypointWrappedFunctions: ['default', 'handler', 'server', 'customExport'], unstable_sentryBundlerPluginOptions: { // Rollup plugin options diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 2be73259305a..359ea36452e9 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -15,6 +15,12 @@ import { SENTRY_WRAPPED_FUNCTIONS, } from '../../src/vite/utils'; +const resolvePathMock = vi.hoisted(() => vi.fn()); + +vi.mock('@nuxt/kit', () => ({ + resolvePath: resolvePathMock, +})); + vi.mock('fs'); describe('findDefaultSdkInitFile', () => { @@ -24,43 +30,83 @@ describe('findDefaultSdkInitFile', () => { it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( 'should return the server file path with .%s extension if it exists', - ext => { + async ext => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return !(filePath instanceof URL) && filePath.toString().includes(`sentry.server.config.${ext}`); }); - const result = findDefaultSdkInitFile('server'); + const result = await findDefaultSdkInitFile('server'); expect(result).toMatch(`packages/nuxt/sentry.server.config.${ext}`); }, ); it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( 'should return the client file path with .%s extension if it exists', - ext => { + async ext => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return !(filePath instanceof URL) && filePath.toString().includes(`sentry.client.config.${ext}`); }); - const result = findDefaultSdkInitFile('client'); + const result = await findDefaultSdkInitFile('client'); expect(result).toMatch(`packages/nuxt/sentry.client.config.${ext}`); }, ); - it('should return undefined if no file with specified extensions exists', () => { + it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( + 'should return a client config from a custom config root dir if it exists with .%s extension', + async ext => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.toString().includes(`sentry.client.config.${ext}`); + }); + + const baseDir = '/users/some-user/front-example/app/config'; + + resolvePathMock.mockResolvedValue(baseDir); + + const result = await findDefaultSdkInitFile('client', undefined, { + configDir: '~/config', + }); + + expect(result).toBe(`${baseDir}/sentry.client.config.${ext}`); + expect(resolvePathMock).toHaveBeenCalledWith('~/config', { type: 'dir' }); + }, + ); + + it.each(['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'])( + 'should return a server config from a custom config root dir if it exists with .%s extension', + async ext => { + vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { + return !(filePath instanceof URL) && filePath.toString().includes(`sentry.server.config.${ext}`); + }); + + const baseDir = '/users/some-user/front-example/app/config'; + + resolvePathMock.mockResolvedValue(baseDir); + + const result = await findDefaultSdkInitFile('server', undefined, { + configDir: '~/config', + }); + + expect(result).toBe(`${baseDir}/sentry.server.config.${ext}`); + expect(resolvePathMock).toHaveBeenCalledWith('~/config', { type: 'dir' }); + }, + ); + + it('should return undefined if no file with specified extensions exists', async () => { vi.spyOn(fs, 'existsSync').mockReturnValue(false); - const result = findDefaultSdkInitFile('server'); + const result = await findDefaultSdkInitFile('server'); expect(result).toBeUndefined(); }); - it('should return undefined if no file exists', () => { + it('should return undefined if no file exists', async () => { vi.spyOn(fs, 'existsSync').mockReturnValue(false); - const result = findDefaultSdkInitFile('server'); + const result = await findDefaultSdkInitFile('server'); expect(result).toBeUndefined(); }); - it('should return the server config file path if server.config and instrument exist', () => { + it('should return the server config file path if server.config and instrument exist', async () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return ( !(filePath instanceof URL) && @@ -69,11 +115,11 @@ describe('findDefaultSdkInitFile', () => { ); }); - const result = findDefaultSdkInitFile('server'); + const result = await findDefaultSdkInitFile('server'); expect(result).toMatch('packages/nuxt/sentry.server.config.js'); }); - it('should return the latest layer config file path if client config exists', () => { + it('should return the latest layer config file path if client config exists', async () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return !(filePath instanceof URL) && filePath.toString().includes('sentry.client.config.ts'); }); @@ -91,11 +137,11 @@ describe('findDefaultSdkInitFile', () => { }, } as unknown as Nuxt; - const result = findDefaultSdkInitFile('client', nuxtMock); + const result = await findDefaultSdkInitFile('client', nuxtMock); expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); }); - it('should return the latest layer config file path if server config exists', () => { + it('should return the latest layer config file path if server config exists', async () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return ( !(filePath instanceof URL) && @@ -117,11 +163,11 @@ describe('findDefaultSdkInitFile', () => { }, } as unknown as Nuxt; - const result = findDefaultSdkInitFile('server', nuxtMock); + const result = await findDefaultSdkInitFile('server', nuxtMock); expect(result).toMatch('packages/nuxt/sentry.server.config.ts'); }); - it('should return the latest layer config file path if client config exists in former layer', () => { + it('should return the latest layer config file path if client config exists in former layer', async () => { vi.spyOn(fs, 'existsSync').mockImplementation(filePath => { return !(filePath instanceof URL) && filePath.toString().includes('nuxt/sentry.client.config.ts'); }); @@ -139,7 +185,7 @@ describe('findDefaultSdkInitFile', () => { }, } as unknown as Nuxt; - const result = findDefaultSdkInitFile('client', nuxtMock); + const result = await findDefaultSdkInitFile('client', nuxtMock); expect(result).toMatch('packages/nuxt/sentry.client.config.ts'); }); });