diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts index 771c534705cb..83e59d439ecf 100644 --- a/packages/nuxt/src/vite/addServerConfig.ts +++ b/packages/nuxt/src/vite/addServerConfig.ts @@ -8,6 +8,7 @@ import type { SentryNuxtModuleOptions } from '../common/types'; import { constructFunctionReExport, constructWrappedFunctionExportQuery, + getExternalOptionsWithSentryNuxt, getFilenameFromNodeStartCommand, QUERY_END_INDICATOR, removeSentryQueryFromPath, @@ -130,6 +131,13 @@ function injectServerConfigPlugin(nitro: Nitro, serverConfigFile: string, debug? return { name: 'rollup-plugin-inject-sentry-server-config', + options(opts) { + return { + ...opts, + external: getExternalOptionsWithSentryNuxt(opts.external), + }; + }, + buildStart() { const configPath = createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`); diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts index ea2db2bc21b8..1aae2b7ea2ba 100644 --- a/packages/nuxt/src/vite/utils.ts +++ b/packages/nuxt/src/vite/utils.ts @@ -1,6 +1,7 @@ import { consoleSandbox } from '@sentry/core'; import * as fs from 'fs'; import * as path from 'path'; +import type { ExternalOption } from 'rollup'; /** * Find the default SDK init file for the given type (client or server). @@ -52,6 +53,35 @@ export function removeSentryQueryFromPath(url: string): string { return url.replace(regex, ''); } +/** + * Add @sentry/nuxt to the external options of the Rollup configuration to prevent Rollup bundling all dependencies + * that would result in adding imports from OpenTelemetry libraries etc. to the server build. + */ +export function getExternalOptionsWithSentryNuxt(previousExternal: ExternalOption | undefined): ExternalOption { + const sentryNuxt = /^@sentry\/nuxt$/; + let external: ExternalOption; + + if (typeof previousExternal === 'function') { + external = new Proxy(previousExternal, { + apply(target, thisArg, args: [string, string | undefined, boolean]) { + const [source] = args; + if (sentryNuxt.test(source)) { + return true; + } + return Reflect.apply(target, thisArg, args); + }, + }); + } else if (Array.isArray(previousExternal)) { + external = [sentryNuxt, ...previousExternal]; + } else if (previousExternal) { + external = [sentryNuxt, previousExternal]; + } else { + external = sentryNuxt; + } + + return external; +} + /** * 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. diff --git a/packages/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts index 7ffd7654549e..452c2780721f 100644 --- a/packages/nuxt/test/vite/utils.test.ts +++ b/packages/nuxt/test/vite/utils.test.ts @@ -5,6 +5,7 @@ import { constructWrappedFunctionExportQuery, extractFunctionReexportQueryParameters, findDefaultSdkInitFile, + getExternalOptionsWithSentryNuxt, getFilenameFromNodeStartCommand, QUERY_END_INDICATOR, removeSentryQueryFromPath, @@ -296,3 +297,60 @@ export { foo_sentryWrapped as foo }; expect(result).toBe(''); }); }); + +describe('getExternalOptionsWithSentryNuxt', () => { + it('should return sentryExternals when previousExternal is undefined', () => { + const result = getExternalOptionsWithSentryNuxt(undefined); + expect(result).toEqual(/^@sentry\/nuxt$/); + }); + + it('should merge sentryExternals with array previousExternal', () => { + const previousExternal = [/vue/, 'react']; + const result = getExternalOptionsWithSentryNuxt(previousExternal); + expect(result).toEqual([/^@sentry\/nuxt$/, /vue/, 'react']); + }); + + it('should create array with sentryExternals and non-array previousExternal', () => { + const previousExternal = 'vue'; + const result = getExternalOptionsWithSentryNuxt(previousExternal); + expect(result).toEqual([/^@sentry\/nuxt$/, 'vue']); + }); + + it('should create a proxy when previousExternal is a function', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + expect(typeof result).toBe('function'); + expect(result).toBeInstanceOf(Function); + }); + + it('should return true from proxied function when source is @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('@sentry/nuxt', undefined, false); + expect(output).toBe(true); + expect(mockExternalFn).not.toHaveBeenCalled(); + }); + + it('should return false from proxied function and call function when source just includes @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('@sentry/nuxt/dist/index.js', undefined, false); + expect(output).toBe(false); + expect(mockExternalFn).toHaveBeenCalledWith('@sentry/nuxt/dist/index.js', undefined, false); + }); + + it('should call original function when source does not include @sentry/nuxt', () => { + const mockExternalFn = vi.fn().mockReturnValue(false); + const result = getExternalOptionsWithSentryNuxt(mockExternalFn); + + // @ts-expect-error - result is a function + const output = result('vue', undefined, false); + expect(output).toBe(false); + expect(mockExternalFn).toHaveBeenCalledWith('vue', undefined, false); + }); +});