Skip to content

Commit

Permalink
reexport and wrap functions
Browse files Browse the repository at this point in the history
  • Loading branch information
s1gr1d committed Oct 31, 2024
1 parent 0453e92 commit 84ff708
Show file tree
Hide file tree
Showing 5 changed files with 183 additions and 83 deletions.
5 changes: 3 additions & 2 deletions packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,16 @@ export type SentryNuxtModuleOptions = {

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your application entrypoint
* with a dynamic `import()` to ensure all dependencies can be properly instrumented.
* with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported.
* Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* If your application has a different main export that is used to run the application, you can overwrite this by providing an array of export names to wrap.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
asyncFunctionReExports?: string[];
entrypointWrappedFunctions?: 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.
Expand Down
2 changes: 1 addition & 1 deletion packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
asyncFunctionReExports: moduleOptionsParam.asyncFunctionReExports || ['default', 'handler', 'server'],
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand Down
28 changes: 16 additions & 12 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,11 @@ import type { InputPluginOption } from 'rollup';
import type { SentryNuxtModuleOptions } from '../common/types';
import {
QUERY_END_INDICATOR,
SENTRY_FUNCTIONS_REEXPORT,
SENTRY_REEXPORTED_FUNCTIONS,
SENTRY_WRAPPED_ENTRY,
SENTRY_WRAPPED_FUNCTIONS,
constructFunctionReExport,
constructFunctionsReExportQuery,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

Expand Down Expand Up @@ -85,8 +86,8 @@ export function addServerConfigToBuild(
export function addDynamicImportEntryFileWrapper(
nitro: Nitro,
serverConfigFile: string,
moduleOptions: Omit<SentryNuxtModuleOptions, 'asyncFunctionReExports'> &
Required<Pick<SentryNuxtModuleOptions, 'asyncFunctionReExports'>>,
moduleOptions: Omit<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'> &
Required<Pick<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'>>,
): void {
if (!nitro.options.rollupConfig) {
nitro.options.rollupConfig = { output: {} };
Expand All @@ -102,7 +103,7 @@ export function addDynamicImportEntryFileWrapper(
nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
asyncFunctionReExports: moduleOptions.asyncFunctionReExports,
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}
Expand All @@ -114,9 +115,9 @@ export function addDynamicImportEntryFileWrapper(
*/
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
asyncFunctionReExports,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; asyncFunctionReExports: string[]; debug?: boolean }): InputPluginOption {
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand Down Expand Up @@ -148,7 +149,9 @@ function wrapEntryWithDynamicImport({
: 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(constructFunctionsReExportQuery(moduleInfo.exportedBindings, asyncFunctionReExports, debug))
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All @@ -158,9 +161,10 @@ function wrapEntryWithDynamicImport({
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedAsyncFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT)
? constructFunctionReExport(id, entryId)
: '';
const reExportedFunctions =
id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS)
? constructFunctionReExport(id, entryId)
: '';

return (
// Regular `import` of the Sentry config
Expand All @@ -170,7 +174,7 @@ function wrapEntryWithDynamicImport({
`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" +
`${reExportedAsyncFunctions}\n`
`${reExportedFunctions}\n`
);
}

Expand Down
104 changes: 72 additions & 32 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde
}

export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry';
export const SENTRY_FUNCTIONS_REEXPORT = '?sentry-query-functions-reexport=';
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';

/**
Expand All @@ -49,63 +50,102 @@ export function removeSentryQueryFromPath(url: string): string {
*
* Only exported for testing.
*/
export function extractFunctionReexportQueryParameters(query: string): string[] {
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 regex = new RegExp(`\\${SENTRY_FUNCTIONS_REEXPORT}(.*?)\\${QUERY_END_INDICATOR}`);
const match = query.match(regex);

return match && match[1]
? match[1]
.split(',')
.filter(param => param !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
: [];
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 !== '')
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
: [];

const reexport =
reexportMatch && reexportMatch[1]
? reexportMatch[1]
.split(',')
.filter(param => param !== '' && param !== 'default')
.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
*/
export function constructFunctionsReExportQuery(
export function constructWrappedFunctionExportQuery(
exportedBindings: Record<string, string[]> | null,
asyncFunctionReExports: string[],
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 || {})).filter(functionName =>
asyncFunctionReExports.includes(functionName),
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.length === 0) {
if (debug && functionsToExport.wrap.length === 0) {
consoleSandbox(() =>
// eslint-disable-next-line no-console
console.warn(
"[Sentry] No functions found for re-export. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.asyncFunctionReExports` in your `nuxt.config.ts`.",
"[Sentry] No functions found to wrap. In case your server needs to export async functions other than `handler` or `server`, consider adding the name(s) to Sentry's build options `sentry.entrypointWrappedFunctions` in your `nuxt.config.ts`.",
),
);
}

return functionsToExport?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(functionsToExport.join(',')) : '';
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].filter(Boolean).join('');
}

/**
* Constructs a code snippet with function reexports (can be used in Rollup plugins)
* 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 functionNames = extractFunctionReexportQueryParameters(pathWithQuery);

return functionNames.reduce(
(functionsCode, currFunctionName) =>
functionsCode.concat(
`async function reExport${currFunctionName.toUpperCase()}(...args) {\n` +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport${currFunctionName.toUpperCase()} as ${currFunctionName} };\n`,
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

0 comments on commit 84ff708

Please sign in to comment.