Skip to content

Commit

Permalink
feat(nuxt): Add option autoInjectServerSentry (no default import())
Browse files Browse the repository at this point in the history
  • Loading branch information
s1gr1d committed Dec 3, 2024
1 parent 97abe0a commit b8caf04
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 60 deletions.
33 changes: 23 additions & 10 deletions packages/nuxt/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -103,22 +103,35 @@ export type SentryNuxtModuleOptions = {
debug?: boolean;

/**
* Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register
* necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling)
*
* If this option is `false`, the Sentry SDK won't wrap the server entry file with `import()`. Not wrapping the
* server entry file will disable Sentry on the server-side. When you set this option to `false`, make sure
* to add the Sentry server config with the node `--import` CLI flag to enable Sentry on the server-side.
* Enables (partial) server tracing by automatically injecting Sentry for environments where modifying the node option `--import` is not possible.
*
* **DO NOT** add the node CLI flag `--import` in your node start script, when `dynamicImportForServerEntry` is set to `true` (default).
* **DO NOT** add the node CLI flag `--import` in your node start script, when auto-injecting Sentry.
* This would initialize Sentry twice on the server-side and this leads to unexpected issues.
*
* @default true
* ---
*
* **"top-level-import"**
*
* Enabling basic server tracing with top-level import can be used for environments where modifying the node option `--import` is not possible.
* However, enabling this option only supports limited tracing instrumentation. Only http traces will be collected (but no database-specific traces etc.).
*
* 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.
*
* ---
* **"experimental_dynamic-import"**
*
* Wraps the server entry file with a dynamic `import()`. This will make it possible to preload Sentry and register
* necessary hooks before other code runs. (Node docs: https://nodejs.org/api/module.html#enabling)
*
* If `"experimental_dynamic-import"` is enabled, the Sentry SDK wraps the server entry file with `import()`.
*
* @default undefined
*/
dynamicImportForServerEntry?: boolean;
autoInjectServerSentry?: 'top-level-import' | 'experimental_dynamic-import';

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to wrap your Nitro server entrypoint
* When `autoInjectServerSentry` is set to `"experimental_dynamic-import"`, the SDK will wrap your Nitro server entrypoint
* 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.
*
Expand All @@ -128,7 +141,7 @@ export type SentryNuxtModuleOptions = {
*
* @default ['default', 'handler', 'server']
*/
entrypointWrappedFunctions?: string[];
experimental_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
31 changes: 21 additions & 10 deletions packages/nuxt/src/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import * as path from 'path';
import { addPlugin, addPluginTemplate, addServerPlugin, createResolver, defineNuxtModule } from '@nuxt/kit';
import { consoleSandbox } from '@sentry/core';
import type { SentryNuxtModuleOptions } from './common/types';
import { addDynamicImportEntryFileWrapper, addServerConfigToBuild } from './vite/addServerConfig';
import { addDynamicImportEntryFileWrapper, addSentryTopImport, addServerConfigToBuild } from './vite/addServerConfig';
import { setupSourceMaps } from './vite/sourceMaps';
import { findDefaultSdkInitFile } from './vite/utils';

Expand All @@ -20,8 +20,12 @@ export default defineNuxtModule<ModuleOptions>({
setup(moduleOptionsParam, nuxt) {
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
autoInjectServerSentry: moduleOptionsParam.autoInjectServerSentry,
experimental_entrypointWrappedFunctions: moduleOptionsParam.experimental_entrypointWrappedFunctions || [
'default',
'handler',
'server',
],
};

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

if (serverConfigFile) {
if (moduleOptions.dynamicImportForServerEntry === false) {
// todo: check when this is needed - seems to be needed for sentry-release-injection-file
if (!moduleOptions.autoInjectServerSentry) {
// Inject the server-side Sentry config file with a side effect import
addPluginTemplate({
mode: 'server',
filename: 'sentry-server-config.mjs',
getContents: () =>
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}"\n` +
'import { defineNuxtPlugin } from "#imports"\n' +
'export default defineNuxtPlugin(() => {})',
`import "${buildDirResolver.resolve(`/${serverConfigFile}`)}";
import { defineNuxtPlugin } from "#imports";
export default defineNuxtPlugin(() => {});`,
});
}

Expand All @@ -79,12 +84,12 @@ export default defineNuxtModule<ModuleOptions>({
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.log(
'[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.',
'[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.',
);
});
}

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

if (moduleOptions.debug) {
Expand All @@ -101,7 +106,13 @@ export default defineNuxtModule<ModuleOptions>({
);
});
}
} else {
}

if (moduleOptions.autoInjectServerSentry === 'top-level-import') {
addSentryTopImport(moduleOptions, nitro);
}

if (moduleOptions.autoInjectServerSentry === 'experimental_dynamic-import') {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
Expand Down
119 changes: 83 additions & 36 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
SENTRY_WRAPPED_FUNCTIONS,
constructFunctionReExport,
constructWrappedFunctionExportQuery,
getFilenameFromPath,
removeSentryQueryFromPath,
} from './utils';

Expand All @@ -38,41 +39,79 @@ export function addServerConfigToBuild(
(viteInlineConfig.build.rollupOptions.input as { [entryName: string]: string })[SERVER_CONFIG_FILENAME] =
createResolver(nuxt.options.srcDir).resolve(`/${serverConfigFile}`);
}
});

/**
* When the build process is finished, copy the `sentry.server.config` file to the `.output` directory.
* This is necessary because we need to reference this file path in the node --import option.
*/
nitro.hooks.hook('close', async () => {
const buildDirResolver = createResolver(nitro.options.buildDir);
const serverDirResolver = createResolver(nitro.options.output.serverDir);
const source = buildDirResolver.resolve(`dist/server/${SERVER_CONFIG_FILENAME}.mjs`);
const destination = serverDirResolver.resolve(`${SERVER_CONFIG_FILENAME}.mjs`);

try {
await fs.promises.access(source, fs.constants.F_OK);
await fs.promises.copyFile(source, destination);

if (moduleOptions.debug) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.log(
`[Sentry] Successfully added the content of the \`${serverConfigFile}\` file to \`${destination}\``,
);
});
}
} catch (error) {
if (moduleOptions.debug) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] An error occurred when trying to add the \`${serverConfigFile}\` file to the \`.output\` directory`,
error,
);
});
}
}
});
}

/**
* Adds the Sentry server config import at the top of the server entry file to load the SDK on the server.
* This is necessary for environments where modifying the node option `--import` is not possible.
* However, only limited tracing instrumentation is supported when doing this.
*/
export function addSentryTopImport(moduleOptions: SentryNuxtModuleOptions, nitro: Nitro): void {
nitro.hooks.hook('close', async () => {
const fileName = nitro.options.commands.preview && getFilenameFromPath(nitro.options.commands.preview);
const serverEntry = fileName ? fileName : 'index.mjs';

const serverDirResolver = createResolver(nitro.options.output.serverDir);
const entryFilePath = serverDirResolver.resolve(serverEntry);

try {
fs.readFile(entryFilePath, 'utf8', (err, data) => {
const updatedContent = `import './${SERVER_CONFIG_FILENAME}.mjs';\n${data}`;

/**
* When the build process is finished, copy the `sentry.server.config` file to the `.output` directory.
* This is necessary because we need to reference this file path in the node --import option.
*/
nitro.hooks.hook('close', async () => {
const buildDirResolver = createResolver(nitro.options.buildDir);
const serverDirResolver = createResolver(nitro.options.output.serverDir);
const source = buildDirResolver.resolve(`dist/server/${SERVER_CONFIG_FILENAME}.mjs`);
const destination = serverDirResolver.resolve(`${SERVER_CONFIG_FILENAME}.mjs`);

try {
await fs.promises.access(source, fs.constants.F_OK);
await fs.promises.copyFile(source, destination);

if (moduleOptions.debug) {
consoleSandbox(() => {
fs.writeFile(entryFilePath, updatedContent, 'utf8', () => {
if (moduleOptions.debug) {
// eslint-disable-next-line no-console
console.log(
`[Sentry] Successfully added the content of the \`${serverConfigFile}\` file to \`${destination}\``,
`[Sentry] Successfully added the Sentry import to the server entry file "\`${entryFilePath}\`"`,
);
});
}
} catch (error) {
if (moduleOptions.debug) {
consoleSandbox(() => {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] An error occurred when trying to add the \`${serverConfigFile}\` file to the \`.output\` directory`,
error,
);
});
}
}
});
});
} catch (err) {
if (moduleOptions.debug) {
// eslint-disable-next-line no-console
console.warn(
`[Sentry] An error occurred when trying to add the Sentry import to the server entry file "\`${entryFilePath}\`":`,
err,
);
}
});
}
});
}

Expand All @@ -86,8 +125,8 @@ export function addServerConfigToBuild(
export function addDynamicImportEntryFileWrapper(
nitro: Nitro,
serverConfigFile: string,
moduleOptions: Omit<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'> &
Required<Pick<SentryNuxtModuleOptions, 'entrypointWrappedFunctions'>>,
moduleOptions: Omit<SentryNuxtModuleOptions, 'experimental_entrypointWrappedFunctions'> &
Required<Pick<SentryNuxtModuleOptions, 'experimental_entrypointWrappedFunctions'>>,
): void {
if (!nitro.options.rollupConfig) {
nitro.options.rollupConfig = { output: {} };
Expand All @@ -103,7 +142,7 @@ export function addDynamicImportEntryFileWrapper(
nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
experimental_entrypointWrappedFunctions: moduleOptions.experimental_entrypointWrappedFunctions,
}),
);
}
Expand All @@ -115,9 +154,13 @@ export function addDynamicImportEntryFileWrapper(
*/
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
experimental_entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
}: {
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
Expand Down Expand Up @@ -156,7 +199,11 @@ function wrapEntryWithDynamicImport({
// 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),
constructWrappedFunctionExportQuery(
moduleInfo.exportedBindings,
experimental_entrypointWrappedFunctions,
debug,
),
)
.concat(QUERY_END_INDICATOR)}`;
}
Expand Down
15 changes: 12 additions & 3 deletions packages/nuxt/src/vite/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,15 @@ export function findDefaultSdkInitFile(type: 'server' | 'client'): string | unde
return filePaths.find(filename => fs.existsSync(filename));
}

/**
* Extracts the filename from a path.
*/
export function getFilenameFromPath(path: string): string | null {
const regex = /[^/\\]+$/;
const match = path.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=';
Expand Down Expand Up @@ -89,7 +98,7 @@ export function extractFunctionReexportQueryParameters(query: string): { wrap: s
*/
export function constructWrappedFunctionExportQuery(
exportedBindings: Record<string, string[]> | null,
entrypointWrappedFunctions: string[],
experimental_entrypointWrappedFunctions: string[],
debug?: boolean,
): string {
const functionsToExport: { wrap: string[]; reexport: string[] } = {
Expand All @@ -101,7 +110,7 @@ export function constructWrappedFunctionExportQuery(
// 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)) {
if (experimental_entrypointWrappedFunctions.includes(fn)) {
functionsToExport.wrap.push(fn);
} else {
functionsToExport.reexport.push(fn);
Expand All @@ -113,7 +122,7 @@ export function constructWrappedFunctionExportQuery(
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`.",
"[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`.",
),
);
}
Expand Down
Loading

0 comments on commit b8caf04

Please sign in to comment.