Skip to content

fix(nuxt): Add @sentry/nuxt as external in Rollup #16407

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
May 28, 2025
Merged
Show file tree
Hide file tree
Changes from 2 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
8 changes: 8 additions & 0 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { SentryNuxtModuleOptions } from '../common/types';
import {
constructFunctionReExport,
constructWrappedFunctionExportQuery,
getExternalOptionsWithSentryNuxt,
getFilenameFromNodeStartCommand,
QUERY_END_INDICATOR,
removeSentryQueryFromPath,
Expand Down Expand Up @@ -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}`);

Expand Down
32 changes: 32 additions & 0 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
@@ -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).
Expand Down Expand Up @@ -52,6 +53,37 @@ 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 sentryExternals = [/^@sentry\/nuxt$/];
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Technically, I don't think this needs to be an array at all, right? Just thinking that the .some call below as well as the object spreads are kinda unnecessary if we only have one element in the array (in the sense of a slight perf overhead as well as code readability).

(I read Andrei's comment, so feel free to ignore this if I happen to review in an in-between state 😅)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Lms24 simplified.

let external: ExternalOption;

if (typeof previousExternal === 'function') {
external = new Proxy(previousExternal, {
apply(target, thisArg, args: [string, string | undefined, boolean]) {
const [source] = args;
if (
sentryExternals.some(external => (typeof external === 'string' ? source === external : external.test(source)))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

l: Are we ever expecting more than one external here? The comment and function name make it sound like it's always only going to be @sentry/nuxt and nothing else. I feel like we can just simplify this code to /^@sentry\/nuxt$/.test(source)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

true, I had 'import-in-the-middle' added to the array at first as well. But as this is actually not necessary, I can remove it 👍

) {
return true;
}
return Reflect.apply(target, thisArg, args);
},
});
} else if (Array.isArray(previousExternal)) {
external = [...sentryExternals, ...previousExternal];
} else if (previousExternal) {
external = [...sentryExternals, previousExternal];
} else {
external = sentryExternals;
}

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.
Expand Down
67 changes: 67 additions & 0 deletions packages/nuxt/test/vite/utils.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
constructWrappedFunctionExportQuery,
extractFunctionReexportQueryParameters,
findDefaultSdkInitFile,
getExternalOptionsWithSentryNuxt,
getFilenameFromNodeStartCommand,
QUERY_END_INDICATOR,
removeSentryQueryFromPath,
Expand Down Expand Up @@ -296,3 +297,69 @@ 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);

if (typeof result === 'function') {
const output = result('@sentry/nuxt', undefined, false);
expect(output).toBe(true);
expect(mockExternalFn).not.toHaveBeenCalled();
} else {
throw Error('Result should be a function');
}
});

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);

if (typeof result === '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);
} else {
throw Error('Result should be a function');
}
});

it('should call original function when source does not include @sentry/nuxt', () => {
const mockExternalFn = vi.fn().mockReturnValue(false);
const result = getExternalOptionsWithSentryNuxt(mockExternalFn);

if (typeof result === 'function') {
const output = result('vue', undefined, false);
expect(output).toBe(false);
expect(mockExternalFn).toHaveBeenCalledWith('vue', undefined, false);
} else {
throw Error('Result should be a function');
}
});
});