Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
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
7 changes: 7 additions & 0 deletions packages/nuxt/build.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { defineBuildConfig } from 'unbuild';

// Build Config for the Nuxt Module Builder: https://github.com/nuxt/module-builder
export default defineBuildConfig({
// The devDependency "@sentry-internal/nitro-utils" triggers "Inlined implicit external", but it's not external
failOnWarn: false,
});
1 change: 1 addition & 0 deletions packages/nuxt/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
},
"devDependencies": {
"@nuxt/module-builder": "^0.8.4",
"@sentry-internal/nitro-utils": "8.42.0",
"nuxt": "^3.13.2"
},
"scripts": {
Expand Down
6 changes: 6 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,12 @@ export type SentryNuxtModuleOptions = {
*/
experimental_entrypointWrappedFunctions?: string[];

/**
* The server entrypoint filename is automatically set by the Sentry SDK depending on the Nitro present.
* In case the server entrypoint has a different filename, you can overwrite it here.
*/
serverEntrypointFileName?: string;

/**
* Options to be passed directly to the Sentry Rollup Plugin (`@sentry/rollup-plugin`) and Sentry Vite Plugin (`@sentry/vite-plugin`) that ship with the Sentry Nuxt SDK.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
112 changes: 9 additions & 103 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,11 @@
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/core';
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,
getFilenameFromNodeStartCommand,
removeSentryQueryFromPath,
} from './utils';
import { getFilenameFromNodeStartCommand } from './utils';

const SERVER_CONFIG_FILENAME = 'sentry.server.config';

Expand Down Expand Up @@ -151,98 +142,13 @@ export function addDynamicImportEntryFileWrapper(
}

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
experimental_entrypointWrappedFunctions: moduleOptions.experimental_entrypointWrappedFunctions,
wrapServerEntryWithDynamicImport({
serverEntrypointFileName: moduleOptions.serverEntrypointFileName || nitro.options.preset,
serverConfigFileName: SERVER_CONFIG_FILENAME,
resolvedServerConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.experimental_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,
experimental_entrypointWrappedFunctions,
debug,
}: {
resolvedSentryConfigPath: string;
experimental_entrypointWrappedFunctions: string[];
debug?: boolean;
}): InputPluginOption {
// In order to correctly import the server config file
// and dynamically import the nitro runtime, we need to
// mark the resolutionId with '\0raw' to fall into the
// raw chunk group, c.f. https://github.com/nitrojs/nitro/commit/8b4a408231bdc222569a32ce109796a41eac4aa6#diff-e58102d2230f95ddeef2662957b48d847a6e891e354cfd0ae6e2e03ce848d1a2R142
const resolutionIdPrefix = '\0raw';

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
: `${resolutionIdPrefix}${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,
experimental_entrypointWrappedFunctions,
debug,
),
)
.concat(QUERY_END_INDICATOR)}`;
}
return null;
},
load(id: string) {
if (id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)) {
const entryId = removeSentryQueryFromPath(id).slice(resolutionIdPrefix.length);

// 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;
},
};
}
130 changes: 0 additions & 130 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox } from '@sentry/core';

/**
* Find the default SDK init file for the given type (client or server).
Expand Down Expand Up @@ -34,132 +33,3 @@ export function getFilenameFromNodeStartCommand(nodeCommand: string): string | n
const match = nodeCommand.match(regex);
return match ? match[0] : null;
}

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<string, string[]> | null,
entrypointWrappedFunctions: string[],
debug?: boolean,
): string {
const functionsToExport: { wrap: string[]; reexport: string[] } = {
wrap: [],
reexport: [],
};

// `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.
Object.values(exportedBindings || {}).forEach(functions =>
functions.forEach(fn => {
if (entrypointWrappedFunctions.includes(fn)) {
functionsToExport.wrap.push(fn);
} else {
functionsToExport.reexport.push(fn);
}
}),
);

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.experimental_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)};`),
'',
),
);
}
Loading
Loading