Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
13 changes: 13 additions & 0 deletions packages/nuxt/src/common/types.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -117,6 +117,19 @@ export type SentryNuxtModuleOptions = {
*/
dynamicImportForServerEntry?: boolean;
Comment thread
andreiborza marked this conversation as resolved.

/**
* By default—unless you configure `dynamicImportForServerEntry: false`—the SDK will try to 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.
*
* By default, the SDK will wrap the default export as well as a `handler` or `server` export from the entrypoint.
* 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.
* Any wrapped export is expected to be an async function.
*
* @default ['default', 'handler', 'server']
*/
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.
* You can use this option to override any options the SDK passes to the Vite (for Nuxt) and Rollup (for Nitro) plugin.
Expand Down
3 changes: 2 additions & 1 deletion packages/nuxt/src/module.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -21,6 +21,7 @@ export default defineNuxtModule<ModuleOptions>({
const moduleOptions = {
...moduleOptionsParam,
dynamicImportForServerEntry: moduleOptionsParam.dynamicImportForServerEntry !== false, // default: true
entrypointWrappedFunctions: moduleOptionsParam.entrypointWrappedFunctions || ['default', 'handler', 'server'],
};

const moduleDirResolver = createResolver(import.meta.url);
Expand DownExpand Up@@ -101,7 +102,7 @@ export default defineNuxtModule<ModuleOptions>({
});
}
} else {
addDynamicImportEntryFileWrapper(nitro, serverConfigFile);
addDynamicImportEntryFileWrapper(nitro, serverConfigFile, moduleOptions);

if (moduleOptions.debug) {
consoleSandbox(() => {
Expand Down
39 changes: 26 additions & 13 deletions packages/nuxt/src/vite/addServerConfig.ts
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
import * as fs from 'fs';
import { createResolver } from '@nuxt/kit';
import type { Nuxt } from '@nuxt/schema';
import { consoleSandbox, flatten } from '@sentry/utils';
import { consoleSandbox } from '@sentry/utils';
import type { Nitro } from 'nitropack';
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,
constructWrappedFunctionExportQuery,
removeSentryQueryFromPath,
} from './utils';

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

nitro.options.rollupConfig.plugins.push(
wrapEntryWithDynamicImport(createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`)),
wrapEntryWithDynamicImport({
resolvedSentryConfigPath: createResolver(nitro.options.srcDir).resolve(`/${serverConfigFile}`),
entrypointWrappedFunctions: moduleOptions.entrypointWrappedFunctions,
}),
);
}

Expand All@@ -103,7 +113,11 @@ export function addDynamicImportEntryFileWrapper(nitro: Nitro, serverConfigFile:
* 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: string): InputPluginOption {
function wrapEntryWithDynamicImport({
resolvedSentryConfigPath,
entrypointWrappedFunctions,
debug,
}: { resolvedSentryConfigPath: string; entrypointWrappedFunctions: string[]; debug?: boolean }): InputPluginOption {
return {
name: 'sentry-wrap-entry-with-dynamic-import',
async resolveId(source, importer, options) {
Expand All@@ -129,17 +143,15 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug

moduleInfo.moduleSideEffects = true;

// `exportedBindings` can look like this: `{ '.': [ 'handler' ], './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 exportedFunctions = flatten(Object.values(moduleInfo.exportedBindings || {}));

// 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
: 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(exportedFunctions?.length ? SENTRY_FUNCTIONS_REEXPORT.concat(exportedFunctions.join(',')) : '')
.concat(
constructWrappedFunctionExportQuery(moduleInfo.exportedBindings, entrypointWrappedFunctions, debug),
)
.concat(QUERY_END_INDICATOR);
}
return null;
Expand All@@ -149,9 +161,10 @@ function wrapEntryWithDynamicImport(resolvedSentryConfigPath: string): InputPlug
const entryId = removeSentryQueryFromPath(id);

// Mostly useful for serverless `handler` functions
const reExportedFunctions = 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 Down
126 changes: 98 additions & 28 deletions packages/nuxt/src/vite/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
import * as fs from 'fs';
import * as path from 'path';
import { consoleSandbox, flatten } from '@sentry/utils';

/**
* Find the default SDK init file for the given type (client or server).
Expand All@@ -26,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@@ -42,42 +44,110 @@ export function removeSentryQueryFromPath(url: string): string {
}

/**
* Extracts and sanitizes function re-export query parameters from a query string.
* If it is a default export, it is not considered for re-exporting. This function is mostly relevant for re-exporting
* serverless `handler` functions.
* 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): 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 !== '')
// Sanitize, as code could be injected with another rollup plugin
.map((str: string) => str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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, '\\$&'))
Comment thread
s1gr1d marked this conversation as resolved.
: [];

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 {
// `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 || {})).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.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.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)
* 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(...args) {\n' +
` const res = await import(${JSON.stringify(entryId)});\n` +
` return res.${currFunctionName}.call(this, ...args);\n` +
'}\n' +
`export { reExport 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