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
680 changes: 0 additions & 680 deletions packages/nextjs/src/config/withSentryConfig.ts

This file was deleted.

114 changes: 114 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/buildTime.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import * as childProcess from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import type { NextConfigObject, SentryBuildOptions } from '../types';

/**
* Adds Sentry-related build-time variables to `nextConfig.env`.
*
* Note: this mutates `userNextConfig`.
*
* @param userNextConfig - The user's Next.js config object
* @param userSentryOptions - The Sentry build options passed to `withSentryConfig`
* @param releaseName - The resolved release name, if any
*/
export function setUpBuildTimeVariables(
userNextConfig: NextConfigObject,
userSentryOptions: SentryBuildOptions,
releaseName: string | undefined,
): void {
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
const basePath = userNextConfig.basePath ?? '';

const rewritesTunnelPath =
userSentryOptions.tunnelRoute !== undefined &&
userNextConfig.output !== 'export' &&
typeof userSentryOptions.tunnelRoute === 'string'
? `${basePath}${userSentryOptions.tunnelRoute}`
: undefined;

const buildTimeVariables: Record<string, string> = {
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
// characters)
_sentryRewriteFramesDistDir: userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
// `assetPrefix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
_sentryRewriteFramesAssetPrefixPath: assetPrefix
? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
: '',
};

if (userNextConfig.assetPrefix) {
buildTimeVariables._assetsPrefix = userNextConfig.assetPrefix;
}

if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) {
buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true';
}

if (rewritesTunnelPath) {
buildTimeVariables._sentryRewritesTunnelPath = rewritesTunnelPath;
}

if (basePath) {
buildTimeVariables._sentryBasePath = basePath;
}

if (userNextConfig.assetPrefix) {
buildTimeVariables._sentryAssetPrefix = userNextConfig.assetPrefix;
}

if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) {
buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true';
}

if (releaseName) {
buildTimeVariables._sentryRelease = releaseName;
}

if (typeof userNextConfig.env === 'object') {
userNextConfig.env = { ...buildTimeVariables, ...userNextConfig.env };
} else if (userNextConfig.env === undefined) {
userNextConfig.env = buildTimeVariables;
}
}

/**
* Returns the current git SHA (HEAD), if available.
*
* This is a best-effort helper and returns `undefined` if git isn't available or the cwd isn't a git repo.
*/
export function getGitRevision(): string | undefined {
let gitRevision: string | undefined;
try {
gitRevision = childProcess
.execSync('git rev-parse HEAD', { stdio: ['ignore', 'pipe', 'ignore'] })
.toString()
.trim();
} catch {
// noop
}
return gitRevision;
}

/**
* Reads the project's `instrumentation-client.(js|ts)` file contents, if present.
*
* @returns The file contents, or `undefined` if the file can't be found/read
*/
export function getInstrumentationClientFileContents(): string | void {
const potentialInstrumentationClientFileLocations = [
['src', 'instrumentation-client.ts'],
['src', 'instrumentation-client.js'],
['instrumentation-client.ts'],
['instrumentation-client.js'],
];

for (const pathSegments of potentialInstrumentationClientFileLocations) {
try {
return fs.readFileSync(path.join(process.cwd(), ...pathSegments), 'utf-8');
} catch {
// noop
}
}
}
32 changes: 32 additions & 0 deletions packages/nextjs/src/config/withSentryConfig/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Packages we auto-instrument need to be external for instrumentation to work
// Next.js externalizes some packages by default, see: https://nextjs.org/docs/app/api-reference/config/next-config-js/serverExternalPackages
// Others we need to add ourselves
//
// NOTE: 'ai' (Vercel AI SDK) is intentionally NOT included in this list.
// When externalized, Next.js doesn't properly handle the package's conditional exports,
// specifically the "react-server" export condition. This causes client-side code to be
// loaded in server components instead of the appropriate server-side functions.
export const DEFAULT_SERVER_EXTERNAL_PACKAGES = [
'amqplib',
'connect',
'dataloader',
'express',
'generic-pool',
'graphql',
'@hapi/hapi',
'ioredis',
'kafkajs',
'koa',
'lru-memoizer',
'mongodb',
'mongoose',
'mysql',
'mysql2',
'knex',
'pg',
'pg-pool',
'@node-redis/client',
'@redis/client',
'redis',
'tedious',
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
import type { SentryBuildOptions } from '../types';
import { detectActiveBundler } from '../util';

/**
* Migrates deprecated top-level webpack options to the new `webpack.*` path for backward compatibility.
* The new path takes precedence over deprecated options. This mutates the userSentryOptions object.
*/
export function migrateDeprecatedWebpackOptions(userSentryOptions: SentryBuildOptions): void {
// Initialize webpack options if not present
userSentryOptions.webpack = userSentryOptions.webpack || {};

const webpack = userSentryOptions.webpack;

const withDeprecatedFallback = <T>(
newValue: T | undefined,
deprecatedValue: T | undefined,
message: string,
): T | undefined => {
if (deprecatedValue !== undefined) {
// eslint-disable-next-line no-console
console.warn(message);
}

return newValue ?? deprecatedValue;
};

const deprecatedMessage = (deprecatedPath: string, newPath: string): string => {
const message = `[@sentry/nextjs] DEPRECATION WARNING: ${deprecatedPath} is deprecated and will be removed in a future version. Use ${newPath} instead.`;

// In Turbopack builds, webpack configuration is not applied, so webpack-scoped options won't have any effect.
if (detectActiveBundler() === 'turbopack' && newPath.startsWith('webpack.')) {
return `${message} (Not supported with Turbopack.)`;
}

return message;
};

/* eslint-disable deprecation/deprecation */
// Migrate each deprecated option to the new path, but only if the new path isn't already set
webpack.autoInstrumentServerFunctions = withDeprecatedFallback(
webpack.autoInstrumentServerFunctions,
userSentryOptions.autoInstrumentServerFunctions,
deprecatedMessage('autoInstrumentServerFunctions', 'webpack.autoInstrumentServerFunctions'),
);

webpack.autoInstrumentMiddleware = withDeprecatedFallback(
webpack.autoInstrumentMiddleware,
userSentryOptions.autoInstrumentMiddleware,
deprecatedMessage('autoInstrumentMiddleware', 'webpack.autoInstrumentMiddleware'),
);

webpack.autoInstrumentAppDirectory = withDeprecatedFallback(
webpack.autoInstrumentAppDirectory,
userSentryOptions.autoInstrumentAppDirectory,
deprecatedMessage('autoInstrumentAppDirectory', 'webpack.autoInstrumentAppDirectory'),
);

webpack.excludeServerRoutes = withDeprecatedFallback(
webpack.excludeServerRoutes,
userSentryOptions.excludeServerRoutes,
deprecatedMessage('excludeServerRoutes', 'webpack.excludeServerRoutes'),
);

webpack.unstable_sentryWebpackPluginOptions = withDeprecatedFallback(
webpack.unstable_sentryWebpackPluginOptions,
userSentryOptions.unstable_sentryWebpackPluginOptions,
deprecatedMessage('unstable_sentryWebpackPluginOptions', 'webpack.unstable_sentryWebpackPluginOptions'),
);

webpack.disableSentryConfig = withDeprecatedFallback(
webpack.disableSentryConfig,
userSentryOptions.disableSentryWebpackConfig,
deprecatedMessage('disableSentryWebpackConfig', 'webpack.disableSentryConfig'),
);

// Handle treeshake.removeDebugLogging specially since it's nested
if (userSentryOptions.disableLogger !== undefined) {
webpack.treeshake = webpack.treeshake || {};
webpack.treeshake.removeDebugLogging = withDeprecatedFallback(
webpack.treeshake.removeDebugLogging,
userSentryOptions.disableLogger,
deprecatedMessage('disableLogger', 'webpack.treeshake.removeDebugLogging'),
);
}

webpack.automaticVercelMonitors = withDeprecatedFallback(
webpack.automaticVercelMonitors,
userSentryOptions.automaticVercelMonitors,
deprecatedMessage('automaticVercelMonitors', 'webpack.automaticVercelMonitors'),
);

webpack.reactComponentAnnotation = withDeprecatedFallback(
webpack.reactComponentAnnotation,
userSentryOptions.reactComponentAnnotation,
deprecatedMessage('reactComponentAnnotation', 'webpack.reactComponentAnnotation'),
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
import type { NextConfigObject, SentryBuildOptions } from '../types';
import { getNextjsVersion } from '../util';
import { setUpBuildTimeVariables } from './buildTime';
import { migrateDeprecatedWebpackOptions } from './deprecatedWebpackOptions';
import {
getBundlerInfo,
getServerExternalPackagesPatch,
getTurbopackPatch,
getWebpackPatch,
maybeConstructTurbopackConfig,
maybeEnableTurbopackSourcemaps,
maybeSetUpRunAfterProductionCompileHook,
maybeWarnAboutUnsupportedRunAfterProductionCompileHook,
maybeWarnAboutUnsupportedTurbopack,
resolveUseRunAfterProductionCompileHookOption,
} from './getFinalConfigObjectBundlerUtils';
import {
getNextMajor,
maybeCreateRouteManifest,
maybeSetClientTraceMetadataOption,
maybeSetInstrumentationHookOption,
maybeSetUpTunnelRouteRewriteRules,
resolveReleaseName,
shouldReturnEarlyInExperimentalBuildMode,
warnIfMissingOnRouterTransitionStartHook,
} from './getFinalConfigObjectUtils';

/**
* Materializes the final Next.js config object with Sentry's build-time integrations applied.
*
* Note: this mutates both `incomingUserNextConfigObject` and `userSentryOptions` (to apply defaults/migrations).
*/
export function getFinalConfigObject(
incomingUserNextConfigObject: NextConfigObject,
userSentryOptions: SentryBuildOptions,
): NextConfigObject {
migrateDeprecatedWebpackOptions(userSentryOptions);
const releaseName = resolveReleaseName(userSentryOptions);

maybeSetUpTunnelRouteRewriteRules(incomingUserNextConfigObject, userSentryOptions);

if (shouldReturnEarlyInExperimentalBuildMode()) {
return incomingUserNextConfigObject;
}

const routeManifest = maybeCreateRouteManifest(incomingUserNextConfigObject, userSentryOptions);
setUpBuildTimeVariables(incomingUserNextConfigObject, userSentryOptions, releaseName);

const nextJsVersion = getNextjsVersion();
const nextMajor = getNextMajor(nextJsVersion);

maybeSetClientTraceMetadataOption(incomingUserNextConfigObject, nextJsVersion);
maybeSetInstrumentationHookOption(incomingUserNextConfigObject, nextJsVersion);
warnIfMissingOnRouterTransitionStartHook(userSentryOptions);

const bundlerInfo = getBundlerInfo(nextJsVersion);
maybeWarnAboutUnsupportedTurbopack(nextJsVersion, bundlerInfo);
maybeWarnAboutUnsupportedRunAfterProductionCompileHook(nextJsVersion, userSentryOptions, bundlerInfo);

const turboPackConfig = maybeConstructTurbopackConfig(
incomingUserNextConfigObject,
userSentryOptions,
routeManifest,
nextJsVersion,
bundlerInfo,
);

const shouldUseRunAfterProductionCompileHook = resolveUseRunAfterProductionCompileHookOption(
userSentryOptions,
bundlerInfo,
);

maybeSetUpRunAfterProductionCompileHook({
incomingUserNextConfigObject,
userSentryOptions,
releaseName,
nextJsVersion,
bundlerInfo,
turboPackConfig,
shouldUseRunAfterProductionCompileHook,
});

maybeEnableTurbopackSourcemaps(incomingUserNextConfigObject, userSentryOptions, bundlerInfo);

return {
...incomingUserNextConfigObject,
...getServerExternalPackagesPatch(incomingUserNextConfigObject, nextMajor),
...getWebpackPatch({
incomingUserNextConfigObject,
userSentryOptions,
releaseName,
routeManifest,
nextJsVersion,
shouldUseRunAfterProductionCompileHook,
bundlerInfo,
}),
...getTurbopackPatch(bundlerInfo, turboPackConfig),
};
}
Loading
Loading