Skip to content

Commit b8caf04

Browse files
committed
feat(nuxt): Add option autoInjectServerSentry (no default import())
1 parent 97abe0a commit b8caf04

File tree

5 files changed

+179
-60
lines changed

5 files changed

+179
-60
lines changed

packages/nuxt/src/common/types.ts

+23-10
Original file line numberDiff line numberDiff line change
@@ -103,22 +103,35 @@ export type SentryNuxtModuleOptions = {
103103
debug?: boolean;
104104

105105
/**
106-
* Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register
107-
* necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling)
108106
*
109-
* If this option is `false`, the Sentry SDK won't wrap the server entry file with `import()`. Not wrapping the
110-
* server entry file will disable Sentry on the server-side. When you set this option to `false`, make sure
111-
* to add the Sentry server config with the node `--import` CLI flag to enable Sentry on the server-side.
107+
* Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible.
112108
*
113-
* **DO NOT** add the node CLI flag `--import` in your node start script, when `dynamicImportForServerEntry` is set to `true` (default).
109+
* **DO NOT** add the node CLI flag `--import` in your node start script, when auto-injecting Sentry.
114110
* This would initialize Sentry twice on the server-side and this leads to unexpected issues.
115111
*
116-
* @default true
112+
* ---
113+
*
114+
* **"top-level-import"**
115+
*
116+
* Enabling basic server tracing with top-level import can be used for environments where modifying the node option `--import` is not possible.
117+
* However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.).
118+
*
119+
* If `"top-level-import"` is enabled, the Sentry SDK will import the Sentry server config at the top of the server entry file to load the SDK on the server.
120+
*
121+
* ---
122+
* **"experimental_dynamic-import"**
123+
*
124+
* Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register
125+
* necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling)
126+
*
127+
* If `"experimental_dynamic-import"` is enabled, the Sentry SDK wraps the server entry file with `import()`.
128+
*
129+
* @default undefined
117130
*/
118-
dynamicImportForServerEntry?: boolean;
131+
autoInjectServerSentry?: 'top-level-import' | 'experimental_dynamic-import';
119132

120133
/**
121-
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your Nitro server entrypoint
134+
* When `autoInjectServerSentry` is set to `"experimental_dynamic-import"`, the SDK will wrap your Nitro server entrypoint
122135
* with a dynamic `import()` to ensure all dependencies can be properly instrumented. Any previous exports from the entrypoint are still exported.
123136
* Most exports of the server entrypoint are serverless functions and those are wrapped by Sentry. Other exports stay as-is.
124137
*
@@ -128,7 +141,7 @@ export type SentryNuxtModuleOptions = {
128141
*
129142
* @default ['default', 'handler', 'server']
130143
*/
131-
entrypointWrappedFunctions?: string[];
144+
experimental_entrypointWrappedFunctions?: string[];
132145

133146
/**
134147
* 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.

packages/nuxt/src/module.ts

+21-10
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import * as path from 'path';
22
import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit';
33
import { consoleSandbox } from '@sentry/core';
44
import type { SentryNuxtModuleOptions } from './common/types';
5-
import { addDynamicImportEntryFileWrapper, addServerConfigToBuild } from './vite/addServerConfig';
5+
import { addDynamicImportEntryFileWrapper, addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig';
66
import { setupSourceMaps } from './vite/sourceMaps';
77
import { findDefaultSdkInitFile } from './vite/utils';
88

@@ -20,8 +20,12 @@ export default defineNuxtModule<ModuleOptions>({
2020
setup(moduleOptionsParam, nuxt) {
2121
const moduleOptions = {
2222
...moduleOptionsParam,
23-
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
24-
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
23+
autoInjectServerSentry: moduleOptionsParam.autoInjectServerSentry,
24+
experimental_entrypointWrappedFunctions: moduleOptionsParam.experimental_entrypointWrappedFunctions || [
25+
'default',
26+
'handler',
27+
'server',
28+
],
2529
};
2630

2731
const moduleDirResolver = createResolver(import.meta.url);
@@ -54,15 +58,16 @@ export default defineNuxtModule<ModuleOptions>({
5458
const serverConfigFile = findDefaultSdkInitFile('server');
5559

5660
if (serverConfigFile) {
57-
if (moduleOptions.dynamicImportForServerEntry === false) {
61+
// todo: check when this is needed - seems to be needed for sentry-release-injection-file
62+
if (!moduleOptions.autoInjectServerSentry) {
5863
// Inject the server-side Sentry config file with a side effect import
5964
addPluginTemplate({
6065
mode: 'server',
6166
filename: 'sentry-server-config.mjs',
6267
getContents: () =>
63-
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` +
64-
'import { defineNuxtPlugin } from "#imports"\n' +
65-
'export default defineNuxtPlugin(() => {})',
68+
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}";
69+
import { defineNuxtPlugin } from "#imports";
70+
export default defineNuxtPlugin(() => {});`,
6671
});
6772
}
6873

@@ -79,12 +84,12 @@ export default defineNuxtModule<ModuleOptions>({
7984
consoleSandbox(() => {
8085
// eslint-disable-next-line no-console
8186
console.log(
82-
'[Sentry] Your application is running in development mode. Note: @sentry/nuxt is in beta and may not work as expected on the server-side (Nitro). Errors are reported, but tracing does not work.',
87+
'[Sentry] Your application is running in development mode. Note: @sentry/nuxt does not work as expected on the server-side (Nitro). Errors are reported, but tracing does not work.',
8388
);
8489
});
8590
}
8691

87-
if (moduleOptions.dynamicImportForServerEntry === false) {
92+
if (moduleOptions.autoInjectServerSentry !== 'experimental_dynamic-import') {
8893
addServerConfigToBuild(moduleOptions, nuxt, nitro, serverConfigFile);
8994

9095
if (moduleOptions.debug) {
@@ -101,7 +106,13 @@ export default defineNuxtModule<ModuleOptions>({
101106
);
102107
});
103108
}
104-
} else {
109+
}
110+
111+
if (moduleOptions.autoInjectServerSentry === 'top-level-import') {
112+
addSentryTopImport(moduleOptions, nitro);
113+
}
114+
115+
if (moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import') {
105116
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);
106117

107118
if (moduleOptions.debug) {

packages/nuxt/src/vite/addServerConfig.ts

+83-36
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
SENTRY_WRAPPED_FUNCTIONS,
1313
constructFunctionReExport,
1414
constructWrappedFunctionExportQuery,
15+
getFilenameFromPath,
1516
removeSentryQueryFromPath,
1617
} from './utils';
1718

@@ -38,41 +39,79 @@ export function addServerConfigToBuild(
3839
(viteInlineConfig.build.rollupOptions.input as { [entryName: string]: string })[SERVER_CONFIG_FILENAME] =
3940
createResolver(nuxt.options.srcDir).resolve(`/${serverConfigFile}`);
4041
}
42+
});
43+
44+
/**
45+
* When the build process is finished, copy the `sentry.server.config` file to the `.output` directory.
46+
* This is necessary because we need to reference this file path in the node --import option.
47+
*/
48+
nitro.hooks.hook('close', async () => {
49+
const buildDirResolver = createResolver(nitro.options.buildDir);
50+
const serverDirResolver = createResolver(nitro.options.output.serverDir);
51+
const source = buildDirResolver.resolve(`dist/server/${SERVER_CONFIG_FILENAME}.mjs`);
52+
const destination = serverDirResolver.resolve(`${SERVER_CONFIG_FILENAME}.mjs`);
53+
54+
try {
55+
await fs.promises.access(source, fs.constants.F_OK);
56+
await fs.promises.copyFile(source, destination);
57+
58+
if (moduleOptions.debug) {
59+
consoleSandbox(() => {
60+
// eslint-disable-next-line no-console
61+
console.log(
62+
`[Sentry] Successfully added the content of the \`${serverConfigFile}\` file to \`${destination}\``,
63+
);
64+
});
65+
}
66+
} catch (error) {
67+
if (moduleOptions.debug) {
68+
consoleSandbox(() => {
69+
// eslint-disable-next-line no-console
70+
console.warn(
71+
`[Sentry] An error occurred when trying to add the \`${serverConfigFile}\` file to the \`.output\` directory`,
72+
error,
73+
);
74+
});
75+
}
76+
}
77+
});
78+
}
79+
80+
/**
81+
* Adds the Sentry server config import at the top of the server entry file to load the SDK on the server.
82+
* This is necessary for environments where modifying the node option `--import` is not possible.
83+
* However, only limited tracing instrumentation is supported when doing this.
84+
*/
85+
export function addSentryTopImport(moduleOptions: SentryNuxtModuleOptions, nitro: Nitro): void {
86+
nitro.hooks.hook('close', async () => {
87+
const fileName = nitro.options.commands.preview && getFilenameFromPath(nitro.options.commands.preview);
88+
const serverEntry = fileName ? fileName : 'index.mjs';
89+
90+
const serverDirResolver = createResolver(nitro.options.output.serverDir);
91+
const entryFilePath = serverDirResolver.resolve(serverEntry);
92+
93+
try {
94+
fs.readFile(entryFilePath, 'utf8', (err, data) => {
95+
const updatedContent = `import './${SERVER_CONFIG_FILENAME}.mjs';\n${data}`;
4196

42-
/**
43-
* When the build process is finished, copy the `sentry.server.config` file to the `.output` directory.
44-
* This is necessary because we need to reference this file path in the node --import option.
45-
*/
46-
nitro.hooks.hook('close', async () => {
47-
const buildDirResolver = createResolver(nitro.options.buildDir);
48-
const serverDirResolver = createResolver(nitro.options.output.serverDir);
49-
const source = buildDirResolver.resolve(`dist/server/${SERVER_CONFIG_FILENAME}.mjs`);
50-
const destination = serverDirResolver.resolve(`${SERVER_CONFIG_FILENAME}.mjs`);
51-
52-
try {
53-
await fs.promises.access(source, fs.constants.F_OK);
54-
await fs.promises.copyFile(source, destination);
55-
56-
if (moduleOptions.debug) {
57-
consoleSandbox(() => {
97+
fs.writeFile(entryFilePath, updatedContent, 'utf8', () => {
98+
if (moduleOptions.debug) {
5899
// eslint-disable-next-line no-console
59100
console.log(
60-
`[Sentry] Successfully added the content of the \`${serverConfigFile}\` file to \`${destination}\``,
101+
`[Sentry] Successfully added the Sentry import to the server entry file "\`${entryFilePath}\`"`,
61102
);
62-
});
63-
}
64-
} catch (error) {
65-
if (moduleOptions.debug) {
66-
consoleSandbox(() => {
67-
// eslint-disable-next-line no-console
68-
console.warn(
69-
`[Sentry] An error occurred when trying to add the \`${serverConfigFile}\` file to the \`.output\` directory`,
70-
error,
71-
);
72-
});
73-
}
103+
}
104+
});
105+
});
106+
} catch (err) {
107+
if (moduleOptions.debug) {
108+
// eslint-disable-next-line no-console
109+
console.warn(
110+
`[Sentry] An error occurred when trying to add the Sentry import to the server entry file "\`${entryFilePath}\`":`,
111+
err,
112+
);
74113
}
75-
});
114+
}
76115
});
77116
}
78117

@@ -86,8 +125,8 @@ export function addServerConfigToBuild(
86125
export function addDynamicImportEntryFileWrapper(
87126
nitro: Nitro,
88127
serverConfigFile: string,
89-
moduleOptions: Omit<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'> &
90-
Required<Pick<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'>>,
128+
moduleOptions: Omit<SentryNuxtModuleOptions, 'experimental_entrypointWrappedFunctions'> &
129+
Required<Pick<SentryNuxtModuleOptions, 'experimental_entrypointWrappedFunctions'>>,
91130
): void {
92131
if (!nitro.options.rollupConfig) {
93132
nitro.options.rollupConfig = { output: {} };
@@ -103,7 +142,7 @@ export function addDynamicImportEntryFileWrapper(
103142
nitro.options.rollupConfig.plugins.push(
104143
wrapEntryWithDynamicImport({
105144
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
106-
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
145+
experimental_entrypointWrappedFunctions: moduleOptions.experimental_entrypointWrappedFunctions,
107146
}),
108147
);
109148
}
@@ -115,9 +154,13 @@ export function addDynamicImportEntryFileWrapper(
115154
*/
116155
function wrapEntryWithDynamicImport({
117156
resolvedSentryConfigPath,
118-
entrypointWrappedFunctions,
157+
experimental_entrypointWrappedFunctions,
119158
debug,
120-
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
159+
}: {
160+
resolvedSentryConfigPath: string;
161+
experimental_entrypointWrappedFunctions: string[];
162+
debug?: boolean;
163+
}): InputPluginOption {
121164
// In order to correctly import the server config file
122165
// and dynamically import the nitro runtime, we need to
123166
// mark the resolutionId with '\0raw' to fall into the
@@ -156,7 +199,11 @@ function wrapEntryWithDynamicImport({
156199
// 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)
157200
.concat(SENTRY_WRAPPED_ENTRY)
158201
.concat(
159-
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
202+
constructWrappedFunctionExportQuery(
203+
moduleInfo.exportedBindings,
204+
experimental_entrypointWrappedFunctions,
205+
debug,
206+
),
160207
)
161208
.concat(QUERY_END_INDICATOR)}`;
162209
}

packages/nuxt/src/vite/utils.ts

+12-3
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,15 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde
2626
return filePaths.find(filename => fs.existsSync(filename));
2727
}
2828

29+
/**
30+
* Extracts the filename from a path.
31+
*/
32+
export function getFilenameFromPath(path: string): string | null {
33+
const regex = /[^/\\]+$/;
34+
const match = path.match(regex);
35+
return match ? match[0] : null;
36+
}
37+
2938
export const SENTRY_WRAPPED_ENTRY = '?sentry-query-wrapped-entry';
3039
export const SENTRY_WRAPPED_FUNCTIONS = '?sentry-query-wrapped-functions=';
3140
export const SENTRY_REEXPORTED_FUNCTIONS = '?sentry-query-reexported-functions=';
@@ -89,7 +98,7 @@ export function extractFunctionReexportQueryParameters(query: string): { wrap: s
8998
*/
9099
export function constructWrappedFunctionExportQuery(
91100
exportedBindings: Record<string, string[]> | null,
92-
entrypointWrappedFunctions: string[],
101+
experimental_entrypointWrappedFunctions: string[],
93102
debug?: boolean,
94103
): string {
95104
const functionsToExport: { wrap: string[]; reexport: string[] } = {
@@ -101,7 +110,7 @@ export function constructWrappedFunctionExportQuery(
101110
// The key `.` refers to exports within the current file, while other keys show from where exports were imported first.
102111
Object.values(exportedBindings || {}).forEach(functions =>
103112
functions.forEach(fn => {
104-
if (entrypointWrappedFunctions.includes(fn)) {
113+
if (experimental_entrypointWrappedFunctions.includes(fn)) {
105114
functionsToExport.wrap.push(fn);
106115
} else {
107116
functionsToExport.reexport.push(fn);
@@ -113,7 +122,7 @@ export function constructWrappedFunctionExportQuery(
113122
consoleSandbox(() =>
114123
// eslint-disable-next-line no-console
115124
console.warn(
116-
"[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`.",
125+
"[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`.",
117126
),
118127
);
119128
}

0 commit comments

Comments
 (0)