Skip to content
Closed
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
3 changes: 2 additions & 1 deletion packages/nextjs/rollup.npm.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ export default [
'src/config/templates/middlewareWrapperTemplate.ts',
'src/config/templates/pageWrapperTemplate.ts',
'src/config/templates/requestAsyncStorageShim.ts',
'src/config/templates/staticGenerationAsyncStorageShim.ts',
'src/config/templates/sentryInitWrapperTemplate.ts',
'src/config/templates/serverComponentWrapperTemplate.ts',
'src/config/templates/routeHandlerWrapperTemplate.ts',
Expand All @@ -48,10 +49,10 @@ export default [
},
external: [
'@sentry/nextjs',
'next/dist/client/components/request-async-storage',
'__SENTRY_CONFIG_IMPORT_PATH__',
'__SENTRY_WRAPPING_TARGET_FILE__',
'__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__',
'__SENTRY_NEXTJS_STATIC_GENERATION_ASYNC_STORAGE_SHIM__',
],
},
}),
Expand Down
12 changes: 12 additions & 0 deletions packages/nextjs/src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export type ServerComponentContext = {
*/
baggageHeader?: string;
headers?: WebFetchHeaders;
hasStaticBehaviour?: boolean;
};

export interface RouteHandlerContext {
Expand All @@ -34,6 +35,7 @@ export interface RouteHandlerContext {
* @deprecated The SDK will automatically pick up the `baggage` header from the incoming Request object instead.
*/
baggageHeader?: string;
hasStaticBehaviour?: boolean;
}

export type VercelCronsConfig = { path?: string; schedule?: string }[] | undefined;
Expand Down Expand Up @@ -85,3 +87,13 @@ export type AugmentedNextApiResponse = NextApiResponse & {

export type ResponseEndMethod = AugmentedNextApiResponse['end'];
export type WrappedResponseEndMethod = AugmentedNextApiResponse['end'] & WrappedFunction;

export interface StaticGenerationStore {
forceStatic?: boolean;
dynamicShouldError?: boolean;
isStaticGeneration?: boolean;
experimental?: {
ppr?: boolean;
};
ppr?: boolean;
}
6 changes: 3 additions & 3 deletions packages/nextjs/src/common/wrapRouteHandlerWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
): (...args: Parameters<F>) => ReturnType<F> extends Promise<unknown> ? ReturnType<F> : Promise<ReturnType<F>> {
addTracingExtensions();
// eslint-disable-next-line deprecation/deprecation
const { method, parameterizedRoute, baggageHeader, sentryTraceHeader } = context;
const { method, parameterizedRoute, baggageHeader, sentryTraceHeader, hasStaticBehaviour } = context;
return new Proxy(routeHandler, {
apply: (originalFunction, thisArg, args) => {
return runWithAsyncContext(async () => {
Expand All @@ -30,8 +30,8 @@ export function wrapRouteHandlerWithSentry<F extends (...args: any[]) => any>(
}

const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
sentryTraceHeader,
baggageHeader,
hasStaticBehaviour ? undefined : sentryTraceHeader,
hasStaticBehaviour ? undefined : baggageHeader,
);
currentScope.setPropagationContext(propagationContext);

Expand Down
8 changes: 4 additions & 4 deletions packages/nextjs/src/common/wrapServerComponentWithSentry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>
context: ServerComponentContext,
): F {
addTracingExtensions();
const { componentRoute, componentType } = context;
const { componentRoute, componentType, hasStaticBehaviour } = context;

// Even though users may define server components as async functions, for the client bundles
// Next.js will turn them into synchronous functions and it will transform any `await`s into instances of the `use`
Expand All @@ -33,9 +33,9 @@ export function wrapServerComponentWithSentry<F extends (...args: any[]) => any>

let maybePromiseResult;

const completeHeadersDict: Record<string, string> = context.headers
? winterCGHeadersToDict(context.headers)
: {};
const completeHeadersDict: Record<string, string> =
// We should not use any headers whent the component has static behaviour
!hasStaticBehaviour && context.headers ? winterCGHeadersToDict(context.headers) : {};

const { traceparentData, dynamicSamplingContext, propagationContext } = tracingContextFromHeaders(
// eslint-disable-next-line deprecation/deprecation
Expand Down
30 changes: 27 additions & 3 deletions packages/nextjs/src/config/loaders/wrappingLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@ const pageWrapperTemplateCode = fs.readFileSync(pageWrapperTemplatePath, { encod
const middlewareWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'middlewareWrapperTemplate.js');
const middlewareWrapperTemplateCode = fs.readFileSync(middlewareWrapperTemplatePath, { encoding: 'utf8' });

let showedMissingAsyncStorageModuleWarning = false;
let showedMissingRequestAsyncStorageModuleWarning = false;
let showedMissingStaticGenerationAsyncStorageModuleWarning = false;

const sentryInitWrapperTemplatePath = path.resolve(__dirname, '..', 'templates', 'sentryInitWrapperTemplate.js');
const sentryInitWrapperTemplateCode = fs.readFileSync(sentryInitWrapperTemplatePath, { encoding: 'utf8' });
Expand All @@ -49,6 +50,7 @@ export type WrappingLoaderOptions = {
sentryConfigFilePath?: string;
vercelCronsConfig?: VercelCronsConfig;
nextjsRequestAsyncStorageModulePath?: string;
nextjsStaticGenerationAsyncStorageModulePath?: string;
};

/**
Expand All @@ -73,6 +75,7 @@ export default function wrappingLoader(
sentryConfigFilePath,
vercelCronsConfig,
nextjsRequestAsyncStorageModulePath,
nextjsStaticGenerationAsyncStorageModulePath,
} = 'getOptions' in this ? this.getOptions() : this.query;

this.async();
Expand Down Expand Up @@ -190,21 +193,42 @@ export default function wrappingLoader(
nextjsRequestAsyncStorageModulePath,
);
} else {
if (!showedMissingAsyncStorageModuleWarning) {
if (!showedMissingRequestAsyncStorageModuleWarning) {
// eslint-disable-next-line no-console
console.warn(
`${chalk.yellow('warn')} - The Sentry SDK could not access the ${chalk.bold.cyan(
'RequestAsyncStorage',
)} module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this.\n`,
);
showedMissingAsyncStorageModuleWarning = true;
showedMissingRequestAsyncStorageModuleWarning = true;
}
templateCode = templateCode.replace(
/__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__/g,
'@sentry/nextjs/esm/config/templates/requestAsyncStorageShim.js',
);
}

if (nextjsStaticGenerationAsyncStorageModulePath !== undefined) {
templateCode = templateCode.replace(
/__SENTRY_NEXTJS_STATIC_GENERATION_ASYNC_STORAGE_SHIM__/g,
nextjsStaticGenerationAsyncStorageModulePath,
);
} else {
if (!showedMissingStaticGenerationAsyncStorageModuleWarning) {
// eslint-disable-next-line no-console
console.warn(
`${chalk.yellow('warn')} - The Sentry SDK could not access the ${chalk.bold.cyan(
'StaticGenerationAsyncStorage',
)} module. Certain features may not work. There is nothing you can do to fix this yourself, but future SDK updates may resolve this.\n`,
);
showedMissingStaticGenerationAsyncStorageModuleWarning = true;
}
templateCode = templateCode.replace(
/__SENTRY_NEXTJS_STATIC_GENERATION_ASYNC_STORAGE_SHIM__/g,
'@sentry/nextjs/esm/config/templates/staticGenerationAsyncStorageShim.js',
);
}

templateCode = templateCode.replace(/__ROUTE__/g, parameterizedPagesRoute.replace(/\\/g, '\\\\'));

const componentTypeMatch = path.posix
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,18 @@ import * as Sentry from '@sentry/nextjs';
// @ts-expect-error Because we cannot be sure if the RequestAsyncStorage module exists (it is not part of the Next.js public
// API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader.
import { requestAsyncStorage } from '__SENTRY_NEXTJS_REQUEST_ASYNC_STORAGE_SHIM__';
// @ts-expect-error Because we cannot be sure if the staticGenerationAsyncStorage module exists (it is not part of the Next.js public
// API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader.
import { staticGenerationAsyncStorage } from '__SENTRY_NEXTJS_STATIC_GENERATION_ASYNC_STORAGE_SHIM__';
// @ts-expect-error See above
import * as routeModule from '__SENTRY_WRAPPING_TARGET_FILE__';
import type { StaticGenerationStore } from '../../common/types';

import type { RequestAsyncStorage } from './requestAsyncStorageShim';
import type { StaticGenerationAsyncStorage } from './staticGenerationAsyncStorageShim';

declare const requestAsyncStorage: RequestAsyncStorage;
declare const staticGenerationAsyncStorage: StaticGenerationAsyncStorage;

declare const routeModule: {
GET?: (...args: unknown[]) => unknown;
Expand All @@ -19,6 +25,16 @@ declare const routeModule: {
OPTIONS?: (...args: unknown[]) => unknown;
};

function storeHasStaticBehaviour(staticGenerationStore: StaticGenerationStore): boolean {
return !!(
staticGenerationStore?.forceStatic ||
staticGenerationStore?.isStaticGeneration ||
staticGenerationStore?.dynamicShouldError ||
staticGenerationStore?.experimental?.ppr ||
staticGenerationStore?.ppr
);
}

function wrapHandler<T>(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS'): T {
// Running the instrumentation code during the build phase will mark any function as "dynamic" because we're accessing
// the Request object. We do not want to turn handlers dynamic so we skip instrumentation in the build phase.
Expand All @@ -44,12 +60,23 @@ function wrapHandler<T>(handler: T, method: 'GET' | 'POST' | 'PUT' | 'PATCH' | '
/** empty */
}

let hasStaticBehaviour: boolean | undefined = false;
try {
const staticGenerationStore = staticGenerationAsyncStorage.getStore();
if (staticGenerationStore) {
hasStaticBehaviour = storeHasStaticBehaviour(staticGenerationStore);
}
} catch (e) {
/** empty */
}

// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
return Sentry.wrapRouteHandlerWithSentry(originalFunction as any, {
method,
parameterizedRoute: '__ROUTE__',
sentryTraceHeader,
baggageHeader,
hasStaticBehaviour,
}).apply(thisArg, args);
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,12 +8,31 @@ import * as serverComponentModule from '__SENTRY_WRAPPING_TARGET_FILE__';

import type { RequestAsyncStorage } from './requestAsyncStorageShim';

// @ts-expect-error Because we cannot be sure if the staticGenerationAsyncStorage module exists (it is not part of the Next.js public
// API) we use a shim if it doesn't exist. The logic for this is in the wrapping loader.
import { staticGenerationAsyncStorage } from '__SENTRY_NEXTJS_STATIC_GENERATION_ASYNC_STORAGE_SHIM__';

import type { StaticGenerationStore } from '../../common/types';
import type { StaticGenerationAsyncStorage } from './staticGenerationAsyncStorageShim';

declare const staticGenerationAsyncStorage: StaticGenerationAsyncStorage;

declare const requestAsyncStorage: RequestAsyncStorage;

declare const serverComponentModule: {
default: unknown;
};

function storeHasStaticBehaviour(staticGenerationStore: StaticGenerationStore): boolean {
return !!(
staticGenerationStore?.forceStatic ||
staticGenerationStore?.isStaticGeneration ||
staticGenerationStore?.dynamicShouldError ||
staticGenerationStore?.experimental?.ppr ||
staticGenerationStore?.ppr
);
}

const serverComponent = serverComponentModule.default;

let wrappedServerComponent;
Expand All @@ -37,12 +56,23 @@ if (typeof serverComponent === 'function') {
/** empty */
}

let hasStaticBehaviour: boolean | undefined = false;
try {
const staticGenerationStore = staticGenerationAsyncStorage.getStore();
if (staticGenerationStore) {
hasStaticBehaviour = storeHasStaticBehaviour(staticGenerationStore);
}
} catch (e) {
/** empty */
}

return Sentry.wrapServerComponentWithSentry(originalFunction, {
componentRoute: '__ROUTE__',
componentType: '__COMPONENT_TYPE__',
sentryTraceHeader,
baggageHeader,
headers,
hasStaticBehaviour,
}).apply(thisArg, args);
},
});
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { StaticGenerationStore } from '../../common/types';

// Vendored from https://github.com/vercel/next.js/blob/445e70502834540d476b8eeaed0228241acd92eb/packages/next/src/client/components/static-generation-async-storage.external.ts
export interface StaticGenerationAsyncStorage {
getStore: () => StaticGenerationStore | undefined;
}
40 changes: 28 additions & 12 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,24 @@ let showedMissingProjectSlugErrorMsg = false;
let showedHiddenSourceMapsWarningMsg = false;
let showedMissingCliBinaryWarningMsg = false;

const POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS = [
// Original location of RequestAsyncStorage
// https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts
'next/dist/client/components/request-async-storage.js',
// Introduced in Next.js 13.4.20
// https://github.com/vercel/next.js/blob/e1bc270830f2fc2df3542d4ef4c61b916c802df3/packages/next/src/client/components/request-async-storage.external.ts
'next/dist/client/components/request-async-storage.external.js',
];

const POTENTIAL_STATIC_GENERATION_ASNYC_STORAGE_LOCATIONS = [
// Original location of StaticGenerationAsyncStorage
// https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/static-generation-async-storage.ts
'next/dist/client/components/static-generation-async-storage.js',
// Introduced in Next.js 13.4.20
// https://github.com/vercel/next.js/blob/e1bc270830f2fc2df3542d4ef4c61b916c802df3/packages/next/src/client/components/static-generation-async-storage.external.ts
'next/dist/client/components/static-generation-async-storage.external.js',
];

// TODO: merge default SentryWebpackPlugin ignore with their SentryWebpackPlugin ignore or ignoreFile
// TODO: merge default SentryWebpackPlugin include with their SentryWebpackPlugin include
// TODO: drop merged keys from override check? `includeDefaults` option?
Expand Down Expand Up @@ -136,7 +154,13 @@ export function constructWebpackConfigFunction(
pageExtensionRegex,
excludeServerRoutes: userSentryOptions.excludeServerRoutes,
sentryConfigFilePath: getUserConfigFilePath(projectDir, runtime),
nextjsRequestAsyncStorageModulePath: getRequestAsyncStorageModuleLocation(
nextjsRequestAsyncStorageModulePath: getNextjsModuleLocation(
POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS,
projectDir,
rawNewConfig.resolve?.modules,
),
nextjsStaticGenerationAsyncStorageModulePath: getNextjsModuleLocation(
POTENTIAL_STATIC_GENERATION_ASNYC_STORAGE_LOCATIONS,
projectDir,
rawNewConfig.resolve?.modules,
),
Expand Down Expand Up @@ -1016,16 +1040,8 @@ function resolveNextPackageDirFromDirectory(basedir: string): string | undefined
}
}

const POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS = [
// Original location of RequestAsyncStorage
// https://github.com/vercel/next.js/blob/46151dd68b417e7850146d00354f89930d10b43b/packages/next/src/client/components/request-async-storage.ts
'next/dist/client/components/request-async-storage.js',
// Introduced in Next.js 13.4.20
// https://github.com/vercel/next.js/blob/e1bc270830f2fc2df3542d4ef4c61b916c802df3/packages/next/src/client/components/request-async-storage.external.ts
'next/dist/client/components/request-async-storage.external.js',
];

function getRequestAsyncStorageModuleLocation(
function getNextjsModuleLocation(
potentialLocations: string[],
webpackContextDir: string,
webpackResolvableModuleLocations: string[] | undefined,
): string | undefined {
Expand All @@ -1040,7 +1056,7 @@ function getRequestAsyncStorageModuleLocation(
for (const webpackResolvableLocation of absoluteWebpackResolvableModuleLocations) {
const nextPackageDir = resolveNextPackageDirFromDirectory(webpackResolvableLocation);
if (nextPackageDir) {
const asyncLocalStorageLocation = POTENTIAL_REQUEST_ASNYC_STORAGE_LOCATIONS.find(loc =>
const asyncLocalStorageLocation = potentialLocations.find(loc =>
fs.existsSync(path.join(nextPackageDir, '..', loc)),
);
if (asyncLocalStorageLocation) {
Expand Down