Skip to content

Commit c579a45

Browse files
authored
Merge pull request #14104 from getsentry/sig/nuxt-reexport-functions
feat(nuxt): Add `entrypointWrappedFunctions` to define async wrapped server functions
2 parents 984a6bd + 62e95b2 commit c579a45

File tree

5 files changed

+260
-60
lines changed

5 files changed

+260
-60
lines changed

packages/nuxt/src/common/types.ts

+13
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
117117
*/
118118
dynamicImportForServerEntry?: boolean;
119119

120+
/**
121+
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your Nitro server entrypoint
122+
* with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported.
123+
* Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is.
124+
*
125+
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
126+
* If your server has a different main export that is used to run the server, you can overwrite this by providing an array of export names to wrap.
127+
* Any wrapped export is expected to be an async function.
128+
*
129+
* @default ['default', 'handler', 'server']
130+
*/
131+
entrypointWrappedFunctions?: string[];
132+
120133
/**
121134
* 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.
122135
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.

packages/nuxt/src/module.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
2121
const moduleOptions = {
2222
...moduleOptionsParam,
2323
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
24+
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
2425
};
2526

2627
const moduleDirResolver = createResolver(import.meta.url);
@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
101102
});
102103
}
103104
} else {
104-
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
105+
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);
105106

106107
if (moduleOptions.debug) {
107108
consoleSandbox(() => {

packages/nuxt/src/vite/addServerConfig.ts

+26-13
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
import * as fs from 'fs';
22
import { createResolver } from '@nuxt/kit';
33
import type { Nuxt } from '@nuxt/schema';
4-
import { consoleSandbox, flatten } from '@sentry/utils';
4+
import { consoleSandbox } from '@sentry/utils';
55
import type { Nitro } from 'nitropack';
66
import type { InputPluginOption } from 'rollup';
77
import type { SentryNuxtModuleOptions } from '../common/types';
88
import {
99
QUERY_END_INDICATOR,
10-
SENTRY_FUNCTIONS_REEXPORT,
10+
SENTRY_REEXPORTED_FUNCTIONS,
1111
SENTRY_WRAPPED_ENTRY,
12+
SENTRY_WRAPPED_FUNCTIONS,
1213
constructFunctionReExport,
14+
constructWrappedFunctionExportQuery,
1315
removeSentryQueryFromPath,
1416
} from './utils';
1517

@@ -81,7 +83,12 @@ export function addServerConfigToBuild(
8183
* With this, the Sentry server config can be loaded before all other modules of the application (which is needed for import-in-the-middle).
8284
* See: https://nodejs.org/api/module.html#enabling
8385
*/
84-
export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile: string): void {
86+
export function addDynamicImportEntryFileWrapper(
87+
nitro: Nitro,
88+
serverConfigFile: string,
89+
moduleOptions: Omit<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'> &
90+
Required<Pick<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'>>,
91+
): void {
8592
if (!nitro.options.rollupConfig) {
8693
nitro.options.rollupConfig = { output: {} };
8794
}
@@ -94,7 +101,10 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
94101
}
95102

96103
nitro.options.rollupConfig.plugins.push(
97-
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
104+
wrapEntryWithDynamicImport({
105+
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
106+
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
107+
}),
98108
);
99109
}
100110

@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
103113
* by using a regular `import` and load the server after that.
104114
* This also works with serverless `handler` functions, as it re-exports the `handler`.
105115
*/
106-
function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPluginOption {
116+
function wrapEntryWithDynamicImport({
117+
resolvedSentryConfigPath,
118+
entrypointWrappedFunctions,
119+
debug,
120+
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
107121
return {
108122
name: 'sentry-wrap-entry-with-dynamic-import',
109123
async resolveId(source, importer, options) {
@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
129143

130144
moduleInfo.moduleSideEffects = true;
131145

132-
// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './firebase-gen-1.mjs': [ 'server' ] }`
133-
// The key `.` refers to exports within the current file, while other keys show from where exports were imported first.
134-
const exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));
135-
136146
// 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
137147
return resolution.id.includes(`.mjs${SENTRY_WRAPPED_ENTRY}`)
138148
? resolution.id
139149
: resolution.id
140150
// 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)
141151
.concat(SENTRY_WRAPPED_ENTRY)
142-
.concat(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
152+
.concat(
153+
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
154+
)
143155
.concat(QUERY_END_INDICATOR);
144156
}
145157
return null;
@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
149161
const entryId = removeSentryQueryFromPath(id);
150162

151163
// Mostly useful for serverless `handler` functions
152-
const reExportedFunctions = id.includes(SENTRY_FUNCTIONS_REEXPORT)
153-
? constructFunctionReExport(id, entryId)
154-
: '';
164+
const reExportedFunctions =
165+
id.includes(SENTRY_WRAPPED_FUNCTIONS) || id.includes(SENTRY_REEXPORTED_FUNCTIONS)
166+
? constructFunctionReExport(id, entryId)
167+
: '';
155168

156169
return (
157170
// Regular `import` of the Sentry config

packages/nuxt/src/vite/utils.ts

+98-28
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import * as fs from 'fs';
22
import * as path from 'path';
3+
import { consoleSandbox, flatten } from '@sentry/utils';
34

45
/**
56
* Find the default SDK init file for the given type (client or server).
@@ -26,7 +27,8 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde
2627
}
2728

2829
export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry';
29-
export const SENTRY_FUNCTIONS_REEXPORT = '?sentry-query-functions-reexport=';
30+
export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions=';
31+
export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions=';
3032
export const QUERY_END_INDICATOR = 'SENTRY-QUERY-END';
3133

3234
/**
@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
4244
}
4345

4446
/**
45-
* Extracts and sanitizes function re-export query parameters from a query string.
46-
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
47-
* serverless `handler` functions.
47+
* Extracts and sanitizes function re-export and function wrap query parameters from a query string.
48+
* If it is a default export, it is not considered for re-exporting.
4849
*
4950
* Only exported for testing.
5051
*/
51-
export function extractFunctionReexportQueryParameters(query: string): string[] {
52+
export function extractFunctionReexportQueryParameters(query: string): { wrap: string[]; reexport: string[] } {
5253
// Regex matches the comma-separated params between the functions query
5354
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
54-
const regex = new RegExp(`\\${SENTRY_FUNCTIONS_REEXPORT}(.*?)\\${QUERY_END_INDICATOR}`);
55-
const match = query.match(regex);
56-
57-
return match && match[1]
58-
? match[1]
59-
.split(',')
60-
.filter(param => param !== '')
61-
// Sanitize, as code could be injected with another rollup plugin
62-
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
63-
: [];
55+
const wrapRegex = new RegExp(
56+
`\\${SENTRY_WRAPPED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR}|\\${SENTRY_REEXPORTED_FUNCTIONS})`,
57+
);
58+
// eslint-disable-next-line @sentry-internal/sdk/no-regexp-constructor
59+
const reexportRegex = new RegExp(`\\${SENTRY_REEXPORTED_FUNCTIONS}(.*?)(\\${QUERY_END_INDICATOR})`);
60+
61+
const wrapMatch = query.match(wrapRegex);
62+
const reexportMatch = query.match(reexportRegex);
63+
64+
const wrap =
65+
wrapMatch && wrapMatch[1]
66+
? wrapMatch[1]
67+
.split(',')
68+
.filter(param => param !== '')
69+
// Sanitize, as code could be injected with another rollup plugin
70+
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
71+
: [];
72+
73+
const reexport =
74+
reexportMatch && reexportMatch[1]
75+
? reexportMatch[1]
76+
.split(',')
77+
.filter(param => param !== '' && param !== 'default')
78+
// Sanitize, as code could be injected with another rollup plugin
79+
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
80+
: [];
81+
82+
return { wrap, reexport };
83+
}
84+
85+
/**
86+
* Constructs a comma-separated string with all functions that need to be re-exported later from the server entry.
87+
* It uses Rollup's `exportedBindings` to determine the functions to re-export. Functions which should be wrapped
88+
* (e.g. serverless handlers) are wrapped by Sentry.
89+
*/
90+
export function constructWrappedFunctionExportQuery(
91+
exportedBindings: Record<string, string[]> | null,
92+
entrypointWrappedFunctions: string[],
93+
debug?: boolean,
94+
): string {
95+
// `exportedBindings` can look like this: `{ '.': [ 'handler' ] }` or `{ '.': [], './firebase-gen-1.mjs': [ 'server' ] }`
96+
// The key `.` refers to exports within the current file, while other keys show from where exports were imported first.
97+
const functionsToExport = flatten(Object.values(exportedBindings || {})).reduce(
98+
(functions, currFunctionName) => {
99+
if (entrypointWrappedFunctions.includes(currFunctionName)) {
100+
functions.wrap.push(currFunctionName);
101+
} else {
102+
functions.reexport.push(currFunctionName);
103+
}
104+
return functions;
105+
},
106+
{ wrap: [], reexport: [] } as { wrap: string[]; reexport: string[] },
107+
);
108+
109+
if (debug && functionsToExport.wrap.length === 0) {
110+
consoleSandbox(() =>
111+
// eslint-disable-next-line no-console
112+
console.warn(
113+
"[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`.",
114+
),
115+
);
116+
}
117+
118+
const wrapQuery = functionsToExport.wrap.length
119+
? `${SENTRY_WRAPPED_FUNCTIONS}${functionsToExport.wrap.join(',')}`
120+
: '';
121+
const reexportQuery = functionsToExport.reexport.length
122+
? `${SENTRY_REEXPORTED_FUNCTIONS}${functionsToExport.reexport.join(',')}`
123+
: '';
124+
125+
return [wrapQuery, reexportQuery].join('');
64126
}
65127

66128
/**
67-
* Constructs a code snippet with function reexports (can be used in Rollup plugins)
129+
* Constructs a code snippet with function reexports (can be used in Rollup plugins as a return value for `load()`)
68130
*/
69131
export function constructFunctionReExport(pathWithQuery: string, entryId: string): string {
70-
const functionNames = extractFunctionReexportQueryParameters(pathWithQuery);
71-
72-
return functionNames.reduce(
73-
(functionsCode, currFunctionName) =>
74-
functionsCode.concat(
75-
'async function reExport(...args) {\n' +
76-
` const res = await import(${JSON.stringify(entryId)});\n` +
77-
` return res.${currFunctionName}.call(this, ...args);\n` +
78-
'}\n' +
79-
`export { reExport as ${currFunctionName} };\n`,
132+
const { wrap: wrapFunctions, reexport: reexportFunctions } = extractFunctionReexportQueryParameters(pathWithQuery);
133+
134+
return wrapFunctions
135+
.reduce(
136+
(functionsCode, currFunctionName) =>
137+
functionsCode.concat(
138+
`async function ${currFunctionName}_sentryWrapped(...args) {\n` +
139+
` const res = await import(${JSON.stringify(entryId)});\n` +
140+
` return res.${currFunctionName}.call(this, ...args);\n` +
141+
'}\n' +
142+
`export { ${currFunctionName}_sentryWrapped as ${currFunctionName} };\n`,
143+
),
144+
'',
145+
)
146+
.concat(
147+
reexportFunctions.reduce(
148+
(functionsCode, currFunctionName) =>
149+
functionsCode.concat(`export { ${currFunctionName} } from ${JSON.stringify(entryId)};`),
150+
'',
80151
),
81-
'',
82-
);
152+
);
83153
}

0 commit comments

Comments
 (0)