diff --git a/package.json b/package.json
index ce3f91932c64..8d7b36e4c1cf 100644
--- a/package.json
+++ b/package.json
@@ -65,6 +65,7 @@
"packages/integration-shims",
"packages/nestjs",
"packages/nextjs",
+ "packages/nitro-utils",
"packages/node",
"packages/nuxt",
"packages/opentelemetry",
diff --git a/packages/nitro-utils/.eslintrc.js b/packages/nitro-utils/.eslintrc.js
new file mode 100644
index 000000000000..3849c1ee28a6
--- /dev/null
+++ b/packages/nitro-utils/.eslintrc.js
@@ -0,0 +1,21 @@
+module.exports = {
+ extends: ['../../.eslintrc.js'],
+ env: {
+ node: true,
+ },
+ overrides: [
+ {
+ files: ['src/**'],
+ rules: {
+ '@sentry-internal/sdk/no-optional-chaining': 'off',
+ },
+ },
+ {
+ files: ['src/metrics/**'],
+ rules: {
+ '@typescript-eslint/explicit-function-return-type': 'off',
+ '@typescript-eslint/no-non-null-assertion': 'off',
+ },
+ },
+ ],
+};
diff --git a/packages/nitro-utils/LICENSE b/packages/nitro-utils/LICENSE
new file mode 100644
index 000000000000..5af93a5bdae5
--- /dev/null
+++ b/packages/nitro-utils/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020-2024 Functional Software, Inc. dba Sentry
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
+of the Software, and to permit persons to whom the Software is furnished to do
+so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/packages/nitro-utils/README.md b/packages/nitro-utils/README.md
new file mode 100644
index 000000000000..304aa0961836
--- /dev/null
+++ b/packages/nitro-utils/README.md
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+# Sentry Utilities for Nitro-based SDKs
+
+[![npm version](https://img.shields.io/npm/v/@sentry-internal/nitro-utils.svg)](https://www.npmjs.com/package/@sentry-internal/nitro-utils)
+[![npm dm](https://img.shields.io/npm/dm/@sentry-internal/nitro-utils.svg)](https://www.npmjs.com/package/@sentry-internal/nitro-utils)
+[![npm dt](https://img.shields.io/npm/dt/@sentry-internal/nitro-utils.svg)](https://www.npmjs.com/package/@sentry-internal/nitro-utils)
+
+## Links
+
+- [Official SDK Docs](https://docs.sentry.io/quickstart/)
+- [TypeDoc](http://getsentry.github.io/sentry-node/)
+
+## General
+
+Common utilities used by Sentry SDKs that use Nitro on the server-side.
+
+Note: This package is only meant to be used internally, and as such is not part of our public API contract and does not
+follow semver.
diff --git a/packages/nitro-utils/package.json b/packages/nitro-utils/package.json
new file mode 100644
index 000000000000..d6bed1ea224e
--- /dev/null
+++ b/packages/nitro-utils/package.json
@@ -0,0 +1,71 @@
+{
+ "name": "@sentry-internal/nitro-utils",
+ "version": "8.36.0",
+ "description": "Utilities for all Sentry SDKs with Nitro on the server-side",
+ "repository": "git://github.com/getsentry/sentry-javascript.git",
+ "homepage": "https://github.com/getsentry/sentry-javascript/tree/master/packages/nitro-utils",
+ "author": "Sentry",
+ "license": "MIT",
+ "private": true,
+ "engines": {
+ "node": ">=14.18"
+ },
+ "files": [
+ "/build"
+ ],
+ "main": "build/cjs/index.js",
+ "module": "build/esm/index.js",
+ "types": "build/types/index.d.ts",
+ "exports": {
+ "./package.json": "./package.json",
+ ".": {
+ "import": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/esm/index.js"
+ },
+ "require": {
+ "types": "./build/types/index.d.ts",
+ "default": "./build/cjs/index.js"
+ }
+ }
+ },
+ "typesVersions": {
+ "<4.9": {
+ "build/types/index.d.ts": [
+ "build/types-ts3.8/index.d.ts"
+ ]
+ }
+ },
+ "publishConfig": {
+ "access": "public"
+ },
+ "dependencies": {
+ "@sentry/core": "8.36.0",
+ "@sentry/types": "8.36.0",
+ "@sentry/utils": "8.36.0"
+ },
+ "scripts": {
+ "build": "run-p build:transpile build:types",
+ "build:dev": "yarn build",
+ "build:transpile": "rollup -c rollup.npm.config.mjs",
+ "build:types": "run-s build:types:core build:types:downlevel",
+ "build:types:core": "tsc -p tsconfig.types.json",
+ "build:types:downlevel": "yarn downlevel-dts build/types build/types-ts3.8 --to ts3.8",
+ "build:watch": "run-p build:transpile:watch build:types:watch",
+ "build:dev:watch": "run-p build:transpile:watch build:types:watch",
+ "build:transpile:watch": "rollup -c rollup.npm.config.mjs --watch",
+ "build:types:watch": "tsc -p tsconfig.types.json --watch",
+ "build:tarball": "npm pack",
+ "clean": "rimraf build coverage sentry-internal-nitro-utils-*.tgz",
+ "fix": "eslint . --format stylish --fix",
+ "lint": "eslint . --format stylish",
+ "test": "yarn test:unit",
+ "test:unit": "vitest run",
+ "test:watch": "vitest --watch",
+ "yalc:publish": "yalc publish --push --sig"
+ },
+ "volta": {
+ "extends": "../../package.json"
+ },
+ "sideEffects": false
+}
diff --git a/packages/nitro-utils/rollup.npm.config.mjs b/packages/nitro-utils/rollup.npm.config.mjs
new file mode 100644
index 000000000000..d28a7a6f54a0
--- /dev/null
+++ b/packages/nitro-utils/rollup.npm.config.mjs
@@ -0,0 +1,17 @@
+import { makeBaseNPMConfig, makeNPMConfigVariants } from '@sentry-internal/rollup-utils';
+
+export default makeNPMConfigVariants(
+ makeBaseNPMConfig({
+ packageSpecificConfig: {
+ output: {
+ // set exports to 'named' or 'auto' so that rollup doesn't warn
+ exports: 'named',
+ // set preserveModules to true because we don't want to bundle everything into one file.
+ preserveModules:
+ process.env.SENTRY_BUILD_PRESERVE_MODULES === undefined
+ ? true
+ : Boolean(process.env.SENTRY_BUILD_PRESERVE_MODULES),
+ },
+ },
+ }),
+);
diff --git a/packages/nitro-utils/src/index.ts b/packages/nitro-utils/src/index.ts
new file mode 100644
index 000000000000..ae57db463f33
--- /dev/null
+++ b/packages/nitro-utils/src/index.ts
@@ -0,0 +1 @@
+export { wrapServerEntryWithDynamicImport } from './rollupPlugins/wrapServerEntryWithDynamicImport';
diff --git a/packages/nitro-utils/src/rollupPlugins/wrapServerEntryWithDynamicImport.ts b/packages/nitro-utils/src/rollupPlugins/wrapServerEntryWithDynamicImport.ts
new file mode 100644
index 000000000000..cf855fc0b68e
--- /dev/null
+++ b/packages/nitro-utils/src/rollupPlugins/wrapServerEntryWithDynamicImport.ts
@@ -0,0 +1,219 @@
+import { consoleSandbox, flatten } from '@sentry/utils';
+import type { InputPluginOption } from 'rollup';
+
+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';
+
+/**
+ * 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.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: {
+ serverConfigFileName: string;
+ resolvedServerConfigPath: string;
+ entrypointWrappedFunctions: string[];
+ additionalImports?: string[];
+ debug?: boolean;
+}): InputPluginOption {
+ const { serverConfigFileName, resolvedServerConfigPath, entrypointWrappedFunctions, additionalImports, debug } =
+ config;
+
+ 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('.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
+ : 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);
+
+ // 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 {
+ // `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.
+ const functionsToExport = flatten(Object.values(exportedBindings || {})).reduce(
+ (functions, currFunctionName) => {
+ if (entrypointWrappedFunctions.includes(currFunctionName)) {
+ functions.wrap.push(currFunctionName);
+ } else {
+ functions.reexport.push(currFunctionName);
+ }
+ return functions;
+ },
+ { wrap: [], reexport: [] } as { wrap: string[]; reexport: string[] },
+ );
+
+ 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 Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.",
+ ),
+ );
+ }
+
+ 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/nitro-utils/test/rollupPlugins/wrapServerEntryWithDynamicImport.test.ts b/packages/nitro-utils/test/rollupPlugins/wrapServerEntryWithDynamicImport.test.ts
new file mode 100644
index 000000000000..1ef8b8d0224c
--- /dev/null
+++ b/packages/nitro-utils/test/rollupPlugins/wrapServerEntryWithDynamicImport.test.ts
@@ -0,0 +1,193 @@
+import { describe, expect, it, vi } from 'vitest';
+import {
+ QUERY_END_INDICATOR,
+ SENTRY_REEXPORTED_FUNCTIONS,
+ SENTRY_WRAPPED_ENTRY,
+ SENTRY_WRAPPED_FUNCTIONS,
+ constructFunctionReExport,
+ constructWrappedFunctionExportQuery,
+ extractFunctionReexportQueryParameters,
+ removeSentryQueryFromPath,
+} from '../../src/rollupPlugins/wrapServerEntryWithDynamicImport';
+
+describe('removeSentryQueryFromPath', () => {
+ it('strips the Sentry query part from the path', () => {
+ const url = `/example/path${SENTRY_WRAPPED_ENTRY}${SENTRY_WRAPPED_FUNCTIONS}foo,${QUERY_END_INDICATOR}`;
+ const url2 = `/example/path${SENTRY_WRAPPED_ENTRY}${QUERY_END_INDICATOR}`;
+ const result = removeSentryQueryFromPath(url);
+ const result2 = removeSentryQueryFromPath(url2);
+ expect(result).toBe('/example/path');
+ expect(result2).toBe('/example/path');
+ });
+
+ it('returns the same path if the specific query part is not present', () => {
+ const url = '/example/path?other-query=param';
+ const result = removeSentryQueryFromPath(url);
+ expect(result).toBe(url);
+ });
+});
+
+describe('extractFunctionReexportQueryParameters', () => {
+ it.each([
+ [`${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }],
+ [
+ `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,default${QUERY_END_INDICATOR}`,
+ { wrap: ['foo', 'bar', 'default'], reexport: [] },
+ ],
+ [
+ `${SENTRY_WRAPPED_FUNCTIONS}foo,a.b*c?d[e]f(g)h|i\\\\j(){hello},${QUERY_END_INDICATOR}`,
+ { wrap: ['foo', 'a\\.b\\*c\\?d\\[e\\]f\\(g\\)h\\|i\\\\\\\\j\\(\\)\\{hello\\}'], reexport: [] },
+ ],
+ [`/example/path/${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }],
+ [
+ `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`,
+ { wrap: ['foo', 'bar'], reexport: [] },
+ ],
+ [`${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`, { wrap: [], reexport: [] }],
+ [
+ `/path${SENTRY_WRAPPED_FUNCTIONS}foo,bar${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`,
+ { wrap: ['foo', 'bar'], reexport: ['bar'] },
+ ],
+ ['?other-query=param', { wrap: [], reexport: [] }],
+ ])('extracts parameters from the query string: %s', (query, expected) => {
+ const result = extractFunctionReexportQueryParameters(query);
+ expect(result).toEqual(expected);
+ });
+});
+
+describe('constructWrappedFunctionExportQuery', () => {
+ it.each([
+ [{ '.': ['handler'] }, ['handler'], `${SENTRY_WRAPPED_FUNCTIONS}handler`],
+ [{ '.': ['handler'], './module': ['server'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}handler,server`],
+ [
+ { '.': ['handler'], './module': ['server'] },
+ ['server'],
+ `${SENTRY_WRAPPED_FUNCTIONS}server${SENTRY_REEXPORTED_FUNCTIONS}handler`,
+ ],
+ [
+ { '.': ['handler', 'otherFunction'] },
+ ['handler'],
+ `${SENTRY_WRAPPED_FUNCTIONS}handler${SENTRY_REEXPORTED_FUNCTIONS}otherFunction`,
+ ],
+ [{ '.': ['handler', 'otherFn'] }, ['handler', 'otherFn'], `${SENTRY_WRAPPED_FUNCTIONS}handler,otherFn`],
+ [{ '.': ['bar'], './module': ['foo'] }, ['bar', 'foo'], `${SENTRY_WRAPPED_FUNCTIONS}bar,foo`],
+ [{ '.': ['foo', 'bar'] }, ['foo'], `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}bar`],
+ [{ '.': ['foo', 'bar'] }, ['bar'], `${SENTRY_WRAPPED_FUNCTIONS}bar${SENTRY_REEXPORTED_FUNCTIONS}foo`],
+ [{ '.': ['foo', 'bar'] }, ['foo', 'bar'], `${SENTRY_WRAPPED_FUNCTIONS}foo,bar`],
+ [{ '.': ['foo', 'bar'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}foo,bar`],
+ ])(
+ 'constructs re-export query for exportedBindings: %j and entrypointWrappedFunctions: %j',
+ (exportedBindings, entrypointWrappedFunctions, expected) => {
+ const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions);
+ expect(result).toBe(expected);
+ },
+ );
+
+ it('logs a warning if no functions are found for re-export and debug is true', () => {
+ const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
+ const exportedBindings = { '.': ['handler'] };
+ const entrypointWrappedFunctions = ['nonExistentFunction'];
+ const debug = true;
+
+ const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions, debug);
+ expect(result).toBe('?sentry-query-reexported-functions=handler');
+ expect(consoleWarnSpy).toHaveBeenCalledWith(
+ "[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 Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.",
+ );
+
+ consoleWarnSpy.mockRestore();
+ });
+});
+
+describe('constructFunctionReExport', () => {
+ it('constructs re-export code for given query parameters and entry ID', () => {
+ const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}}`;
+ const query2 = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}}`;
+ const entryId = './module';
+ const result = constructFunctionReExport(query, entryId);
+ const result2 = constructFunctionReExport(query2, entryId);
+
+ const expected = `
+async function foo_sentryWrapped(...args) {
+ const res = await import("./module");
+ return res.foo.call(this, ...args);
+}
+export { foo_sentryWrapped as foo };
+async function bar_sentryWrapped(...args) {
+ const res = await import("./module");
+ return res.bar.call(this, ...args);
+}
+export { bar_sentryWrapped as bar };
+`;
+ expect(result.trim()).toBe(expected.trim());
+ expect(result2.trim()).toBe(expected.trim());
+ });
+
+ it('constructs re-export code for a "default" query parameters and entry ID', () => {
+ const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`;
+ const entryId = './index';
+ const result = constructFunctionReExport(query, entryId);
+
+ const expected = `
+async function default_sentryWrapped(...args) {
+ const res = await import("./index");
+ return res.default.call(this, ...args);
+}
+export { default_sentryWrapped as default };
+`;
+ expect(result.trim()).toBe(expected.trim());
+ });
+
+ it('constructs re-export code for a "default" query parameters and entry ID', () => {
+ const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`;
+ const entryId = './index';
+ const result = constructFunctionReExport(query, entryId);
+
+ const expected = `
+async function default_sentryWrapped(...args) {
+ const res = await import("./index");
+ return res.default.call(this, ...args);
+}
+export { default_sentryWrapped as default };
+`;
+ expect(result.trim()).toBe(expected.trim());
+ });
+
+ it('constructs re-export code for a mix of wrapped and re-exported functions', () => {
+ const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`;
+ const entryId = './module';
+ const result = constructFunctionReExport(query, entryId);
+
+ const expected = `
+async function foo_sentryWrapped(...args) {
+ const res = await import("./module");
+ return res.foo.call(this, ...args);
+}
+export { foo_sentryWrapped as foo };
+export { bar } from "./module";
+`;
+ expect(result.trim()).toBe(expected.trim());
+ });
+
+ it('does not re-export a default export for regular re-exported functions', () => {
+ const query = `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}default${QUERY_END_INDICATOR}`;
+ const entryId = './module';
+ const result = constructFunctionReExport(query, entryId);
+
+ const expected = `
+async function foo_sentryWrapped(...args) {
+ const res = await import("./module");
+ return res.foo.call(this, ...args);
+}
+export { foo_sentryWrapped as foo };
+`;
+ expect(result.trim()).toBe(expected.trim());
+ });
+
+ it('returns an empty string if the query string is empty', () => {
+ const query = '';
+ const entryId = './module';
+ const result = constructFunctionReExport(query, entryId);
+ expect(result).toBe('');
+ });
+});
diff --git a/packages/nitro-utils/test/tsconfig.json b/packages/nitro-utils/test/tsconfig.json
new file mode 100644
index 000000000000..38ca0b13bcdd
--- /dev/null
+++ b/packages/nitro-utils/test/tsconfig.json
@@ -0,0 +1,3 @@
+{
+ "extends": "../tsconfig.test.json"
+}
diff --git a/packages/nitro-utils/test/vitest.setup.ts b/packages/nitro-utils/test/vitest.setup.ts
new file mode 100644
index 000000000000..7676ce96afef
--- /dev/null
+++ b/packages/nitro-utils/test/vitest.setup.ts
@@ -0,0 +1,8 @@
+export function setup() {}
+
+if (!globalThis.fetch) {
+ // @ts-expect-error - Needed for vitest to work with our fetch instrumentation
+ globalThis.Request = class Request {};
+ // @ts-expect-error - Needed for vitest to work with our fetch instrumentation
+ globalThis.Response = class Response {};
+}
diff --git a/packages/nitro-utils/tsconfig.json b/packages/nitro-utils/tsconfig.json
new file mode 100644
index 000000000000..425f0657515d
--- /dev/null
+++ b/packages/nitro-utils/tsconfig.json
@@ -0,0 +1,9 @@
+{
+ "extends": "../../tsconfig.json",
+
+ "include": ["src/**/*"],
+
+ "compilerOptions": {
+ "lib": ["ES2018"],
+ }
+}
diff --git a/packages/nitro-utils/tsconfig.test.json b/packages/nitro-utils/tsconfig.test.json
new file mode 100644
index 000000000000..3fbe012384ee
--- /dev/null
+++ b/packages/nitro-utils/tsconfig.test.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "include": ["test/**/*", "vite.config.ts"],
+
+ "compilerOptions": {
+ // should include all types from `./tsconfig.json` plus types for all test frameworks used
+ "types": ["node", "vitest/globals"]
+ }
+}
diff --git a/packages/nitro-utils/tsconfig.types.json b/packages/nitro-utils/tsconfig.types.json
new file mode 100644
index 000000000000..65455f66bd75
--- /dev/null
+++ b/packages/nitro-utils/tsconfig.types.json
@@ -0,0 +1,10 @@
+{
+ "extends": "./tsconfig.json",
+
+ "compilerOptions": {
+ "declaration": true,
+ "declarationMap": true,
+ "emitDeclarationOnly": true,
+ "outDir": "build/types"
+ }
+}
diff --git a/packages/nitro-utils/vite.config.ts b/packages/nitro-utils/vite.config.ts
new file mode 100644
index 000000000000..0229ec105e04
--- /dev/null
+++ b/packages/nitro-utils/vite.config.ts
@@ -0,0 +1,9 @@
+import baseConfig from '../../vite/vite.config';
+
+export default {
+ ...baseConfig,
+ test: {
+ environment: 'jsdom',
+ setupFiles: ['./test/vitest.setup.ts'],
+ },
+};
diff --git a/packages/nuxt/package.json b/packages/nuxt/package.json
index c016109c717e..33b8ef888269 100644
--- a/packages/nuxt/package.json
+++ b/packages/nuxt/package.json
@@ -51,7 +51,8 @@
"@sentry/types": "8.36.0",
"@sentry/utils": "8.36.0",
"@sentry/vite-plugin": "2.22.6",
- "@sentry/vue": "8.36.0"
+ "@sentry/vue": "8.36.0",
+ "@sentry-internal/nitro-utils": "8.36.0"
},
"devDependencies": {
"@nuxt/module-builder": "^0.8.4",
diff --git a/packages/nuxt/src/vite/addServerConfig.ts b/packages/nuxt/src/vite/addServerConfig.ts
index cf4b2a95473e..f43107daa7ff 100644
--- a/packages/nuxt/src/vite/addServerConfig.ts
+++ b/packages/nuxt/src/vite/addServerConfig.ts
@@ -1,19 +1,10 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
+import { wrapServerEntryWithDynamicImport } from '@sentry-internal/nitro-utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
-import type { InputPluginOption } from 'rollup';
import type { SentryNuxtModuleOptions } from '../common/types';
-import {
- QUERY_END_INDICATOR,
- SENTRY_REEXPORTED_FUNCTIONS,
- SENTRY_WRAPPED_ENTRY,
- SENTRY_WRAPPED_FUNCTIONS,
- constructFunctionReExport,
- constructWrappedFunctionExportQuery,
- removeSentryQueryFromPath,
-} from './utils';
const SERVER_CONFIG_FILENAME = 'sentry.server.config';
@@ -101,84 +92,12 @@ export function addDynamicImportEntryFileWrapper(
}
nitro.options.rollupConfig.plugins.push(
- wrapEntryWithDynamicImport({
- resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
+ wrapServerEntryWithDynamicImport({
+ serverConfigFileName: SERVER_CONFIG_FILENAME,
+ resolvedServerConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
+ additionalImports: ['import-in-the-middle/hook.mjs'],
+ debug: moduleOptions.debug,
}),
);
}
-
-/**
- * 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`.
- */
-function wrapEntryWithDynamicImport({
- resolvedSentryConfigPath,
- entrypointWrappedFunctions,
- debug,
-}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
- return {
- name: 'sentry-wrap-entry-with-dynamic-import',
- async resolveId(source, importer, options) {
- if (source.includes(`/${SERVER_CONFIG_FILENAME}`)) {
- 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 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(
- 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);
-
- // 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(resolvedSentryConfigPath)};\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 "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';\n" +
- `${reExportedFunctions}\n`
- );
- }
-
- return null;
- },
- };
-}
diff --git a/packages/nuxt/src/vite/utils.ts b/packages/nuxt/src/vite/utils.ts
index 8fffc8fe06c9..e41d3fb06cab 100644
--- a/packages/nuxt/src/vite/utils.ts
+++ b/packages/nuxt/src/vite/utils.ts
@@ -1,6 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
-import { consoleSandbox, flatten } from '@sentry/utils';
/**
* Find the default SDK init file for the given type (client or server).
@@ -25,129 +24,3 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde
return filePaths.find(filename => fs.existsSync(filename));
}
-
-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';
-
-/**
- * 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.
- */
-export function constructWrappedFunctionExportQuery(
- exportedBindings: Record | null,
- entrypointWrappedFunctions: string[],
- debug?: boolean,
-): string {
- // `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.
- const functionsToExport = flatten(Object.values(exportedBindings || {})).reduce(
- (functions, currFunctionName) => {
- if (entrypointWrappedFunctions.includes(currFunctionName)) {
- functions.wrap.push(currFunctionName);
- } else {
- functions.reexport.push(currFunctionName);
- }
- return functions;
- },
- { wrap: [], reexport: [] } as { wrap: string[]; reexport: string[] },
- );
-
- 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 Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.",
- ),
- );
- }
-
- 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()`)
- */
-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/nuxt/test/vite/utils.test.ts b/packages/nuxt/test/vite/utils.test.ts
index a35f9cf8ca34..5115742be0f0 100644
--- a/packages/nuxt/test/vite/utils.test.ts
+++ b/packages/nuxt/test/vite/utils.test.ts
@@ -1,16 +1,6 @@
import * as fs from 'fs';
import { afterEach, describe, expect, it, vi } from 'vitest';
-import {
- QUERY_END_INDICATOR,
- SENTRY_REEXPORTED_FUNCTIONS,
- SENTRY_WRAPPED_ENTRY,
- SENTRY_WRAPPED_FUNCTIONS,
- constructFunctionReExport,
- constructWrappedFunctionExportQuery,
- extractFunctionReexportQueryParameters,
- findDefaultSdkInitFile,
- removeSentryQueryFromPath,
-} from '../../src/vite/utils';
+import { findDefaultSdkInitFile } from '../../src/vite/utils';
vi.mock('fs');
@@ -69,185 +59,3 @@ describe('findDefaultSdkInitFile', () => {
expect(result).toMatch('packages/nuxt/sentry.server.config.js');
});
});
-
-describe('removeSentryQueryFromPath', () => {
- it('strips the Sentry query part from the path', () => {
- const url = `/example/path${SENTRY_WRAPPED_ENTRY}${SENTRY_WRAPPED_FUNCTIONS}foo,${QUERY_END_INDICATOR}`;
- const url2 = `/example/path${SENTRY_WRAPPED_ENTRY}${QUERY_END_INDICATOR}`;
- const result = removeSentryQueryFromPath(url);
- const result2 = removeSentryQueryFromPath(url2);
- expect(result).toBe('/example/path');
- expect(result2).toBe('/example/path');
- });
-
- it('returns the same path if the specific query part is not present', () => {
- const url = '/example/path?other-query=param';
- const result = removeSentryQueryFromPath(url);
- expect(result).toBe(url);
- });
-});
-
-describe('extractFunctionReexportQueryParameters', () => {
- it.each([
- [`${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }],
- [
- `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,default${QUERY_END_INDICATOR}`,
- { wrap: ['foo', 'bar', 'default'], reexport: [] },
- ],
- [
- `${SENTRY_WRAPPED_FUNCTIONS}foo,a.b*c?d[e]f(g)h|i\\\\j(){hello},${QUERY_END_INDICATOR}`,
- { wrap: ['foo', 'a\\.b\\*c\\?d\\[e\\]f\\(g\\)h\\|i\\\\\\\\j\\(\\)\\{hello\\}'], reexport: [] },
- ],
- [`/example/path/${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}`, { wrap: ['foo', 'bar'], reexport: [] }],
- [
- `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`,
- { wrap: ['foo', 'bar'], reexport: [] },
- ],
- [`${SENTRY_REEXPORTED_FUNCTIONS}${QUERY_END_INDICATOR}`, { wrap: [], reexport: [] }],
- [
- `/path${SENTRY_WRAPPED_FUNCTIONS}foo,bar${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`,
- { wrap: ['foo', 'bar'], reexport: ['bar'] },
- ],
- ['?other-query=param', { wrap: [], reexport: [] }],
- ])('extracts parameters from the query string: %s', (query, expected) => {
- const result = extractFunctionReexportQueryParameters(query);
- expect(result).toEqual(expected);
- });
-});
-
-describe('constructWrappedFunctionExportQuery', () => {
- it.each([
- [{ '.': ['handler'] }, ['handler'], `${SENTRY_WRAPPED_FUNCTIONS}handler`],
- [{ '.': ['handler'], './module': ['server'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}handler,server`],
- [
- { '.': ['handler'], './module': ['server'] },
- ['server'],
- `${SENTRY_WRAPPED_FUNCTIONS}server${SENTRY_REEXPORTED_FUNCTIONS}handler`,
- ],
- [
- { '.': ['handler', 'otherFunction'] },
- ['handler'],
- `${SENTRY_WRAPPED_FUNCTIONS}handler${SENTRY_REEXPORTED_FUNCTIONS}otherFunction`,
- ],
- [{ '.': ['handler', 'otherFn'] }, ['handler', 'otherFn'], `${SENTRY_WRAPPED_FUNCTIONS}handler,otherFn`],
- [{ '.': ['bar'], './module': ['foo'] }, ['bar', 'foo'], `${SENTRY_WRAPPED_FUNCTIONS}bar,foo`],
- [{ '.': ['foo', 'bar'] }, ['foo'], `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}bar`],
- [{ '.': ['foo', 'bar'] }, ['bar'], `${SENTRY_WRAPPED_FUNCTIONS}bar${SENTRY_REEXPORTED_FUNCTIONS}foo`],
- [{ '.': ['foo', 'bar'] }, ['foo', 'bar'], `${SENTRY_WRAPPED_FUNCTIONS}foo,bar`],
- [{ '.': ['foo', 'bar'] }, [], `${SENTRY_REEXPORTED_FUNCTIONS}foo,bar`],
- ])(
- 'constructs re-export query for exportedBindings: %j and entrypointWrappedFunctions: %j',
- (exportedBindings, entrypointWrappedFunctions, expected) => {
- const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions);
- expect(result).toBe(expected);
- },
- );
-
- it('logs a warning if no functions are found for re-export and debug is true', () => {
- const consoleWarnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
- const exportedBindings = { '.': ['handler'] };
- const entrypointWrappedFunctions = ['nonExistentFunction'];
- const debug = true;
-
- const result = constructWrappedFunctionExportQuery(exportedBindings, entrypointWrappedFunctions, debug);
- expect(result).toBe('?sentry-query-reexported-functions=handler');
- expect(consoleWarnSpy).toHaveBeenCalledWith(
- "[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 Sentry's build options `sentry.entrypointWrappedFunctions` in `nuxt.config.ts`.",
- );
-
- consoleWarnSpy.mockRestore();
- });
-});
-
-describe('constructFunctionReExport', () => {
- it('constructs re-export code for given query parameters and entry ID', () => {
- const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar,${QUERY_END_INDICATOR}}`;
- const query2 = `${SENTRY_WRAPPED_FUNCTIONS}foo,bar${QUERY_END_INDICATOR}}`;
- const entryId = './module';
- const result = constructFunctionReExport(query, entryId);
- const result2 = constructFunctionReExport(query2, entryId);
-
- const expected = `
-async function foo_sentryWrapped(...args) {
- const res = await import("./module");
- return res.foo.call(this, ...args);
-}
-export { foo_sentryWrapped as foo };
-async function bar_sentryWrapped(...args) {
- const res = await import("./module");
- return res.bar.call(this, ...args);
-}
-export { bar_sentryWrapped as bar };
-`;
- expect(result.trim()).toBe(expected.trim());
- expect(result2.trim()).toBe(expected.trim());
- });
-
- it('constructs re-export code for a "default" query parameters and entry ID', () => {
- const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`;
- const entryId = './index';
- const result = constructFunctionReExport(query, entryId);
-
- const expected = `
-async function default_sentryWrapped(...args) {
- const res = await import("./index");
- return res.default.call(this, ...args);
-}
-export { default_sentryWrapped as default };
-`;
- expect(result.trim()).toBe(expected.trim());
- });
-
- it('constructs re-export code for a "default" query parameters and entry ID', () => {
- const query = `${SENTRY_WRAPPED_FUNCTIONS}default${QUERY_END_INDICATOR}}`;
- const entryId = './index';
- const result = constructFunctionReExport(query, entryId);
-
- const expected = `
-async function default_sentryWrapped(...args) {
- const res = await import("./index");
- return res.default.call(this, ...args);
-}
-export { default_sentryWrapped as default };
-`;
- expect(result.trim()).toBe(expected.trim());
- });
-
- it('constructs re-export code for a mix of wrapped and re-exported functions', () => {
- const query = `${SENTRY_WRAPPED_FUNCTIONS}foo,${SENTRY_REEXPORTED_FUNCTIONS}bar${QUERY_END_INDICATOR}`;
- const entryId = './module';
- const result = constructFunctionReExport(query, entryId);
-
- const expected = `
-async function foo_sentryWrapped(...args) {
- const res = await import("./module");
- return res.foo.call(this, ...args);
-}
-export { foo_sentryWrapped as foo };
-export { bar } from "./module";
-`;
- expect(result.trim()).toBe(expected.trim());
- });
-
- it('does not re-export a default export for regular re-exported functions', () => {
- const query = `${SENTRY_WRAPPED_FUNCTIONS}foo${SENTRY_REEXPORTED_FUNCTIONS}default${QUERY_END_INDICATOR}`;
- const entryId = './module';
- const result = constructFunctionReExport(query, entryId);
-
- const expected = `
-async function foo_sentryWrapped(...args) {
- const res = await import("./module");
- return res.foo.call(this, ...args);
-}
-export { foo_sentryWrapped as foo };
-`;
- expect(result.trim()).toBe(expected.trim());
- });
-
- it('returns an empty string if the query string is empty', () => {
- const query = '';
- const entryId = './module';
- const result = constructFunctionReExport(query, entryId);
- expect(result).toBe('');
- });
-});