Skip to content
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 21 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,27 @@ 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 (`<projectRoot>/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.
*
* Relatives path using Nuxt aliases are supported.
Comment thread
victorgarciaesgi marked this conversation as resolved.
Outdated
* @example
*
* ```ts
* sentry: {
* configDir: '~/sentry-config',
* // Sentry module will search for `<projectRoot>/<srcDir>/sentry-config/sentry.(client|server).config.(js|ts)` files.
Comment thread
victorgarciaesgi marked this conversation as resolved.
Outdated
* }
* ```
*
*
* @default '<projectRoot>'
Comment thread
victorgarciaesgi marked this conversation as resolved.
Outdated
*/
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.
Expand Down
4 changes: 2 additions & 2 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ export default defineNuxtModule<ModuleOptions>({
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
Expand Down Expand Up @@ -78,7 +78,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}

const serverConfigFile = findDefaultSdkInitFile('server', nuxt);
const serverConfigFile = await findDefaultSdkInitFile('server', nuxt, moduleOptions);
Comment thread
victorgarciaesgi marked this conversation as resolved.
Comment thread
cursor[bot] marked this conversation as resolved.
const isNitroV3 = (await getNitroMajorVersion()) >= 3;
const nuxtMajor = parseInt((nuxt as unknown as { _version: string })._version?.split('.')[0] ?? '3', 10);
const isMinNuxtV4 = nuxtMajor >= 4;
Expand Down
17 changes: 14 additions & 3 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -25,7 +27,11 @@ export async function getNitroMajorVersion(): Promise<number> {
* 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<string | undefined> {
const possibleFileExtensions = ['ts', 'js', 'mjs', 'cjs', 'mts', 'cts'];
const relativePaths: string[] = [];

Expand Down Expand Up @@ -53,9 +59,14 @@ export function findDefaultSdkInitFile(type: 'server' | 'client', nuxt?: Nuxt):
}

// As a fallback, also check CWD (left for pure compatibility)
const cwd = process.cwd();
let rootDir: string;
if (options?.configDir) {
rootDir = await resolvePath(options.configDir, { type: 'dir' });
} else {
rootDir = process.cwd();
}
Comment thread
victorgarciaesgi marked this conversation as resolved.
Outdated
for (const relativePath of relativePaths) {
const fullPath = path.resolve(cwd, relativePath);
const fullPath = path.resolve(rootDir, relativePath);
if (fs.existsSync(fullPath)) {
Comment thread
sentry[bot] marked this conversation as resolved.
return fullPath;
}
Expand Down
1 change: 1 addition & 0 deletions packages/nuxt/test/vite/buildOptions.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
78 changes: 62 additions & 16 deletions packages/nuxt/test/vite/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand All @@ -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) &&
Expand All @@ -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');
});
Expand All @@ -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) &&
Expand All @@ -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');
});
Expand All @@ -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');
});
});
Expand Down