Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(nextjs): Use afterProductionBuild to upload sourcemaps and do release management #15779

Draft
wants to merge 5 commits into
base: develop
Choose a base branch
from
Draft
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
4 changes: 3 additions & 1 deletion packages/nextjs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,10 @@
"@sentry/opentelemetry": "9.10.1",
"@sentry/react": "9.10.1",
"@sentry/vercel-edge": "9.10.1",
"@sentry/webpack-plugin": "3.2.4",
"@sentry/webpack-plugin": "3.3.0-alpha.1",
"@sentry/bundler-plugin-core": "3.3.0-alpha.1",
"chalk": "3.0.0",
"glob": "^9.3.2",
"resolve": "1.22.8",
"rollup": "4.35.0",
"stacktrace-parser": "^0.1.10"
Expand Down
46 changes: 46 additions & 0 deletions packages/nextjs/src/config/afterProductionBuild.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import type { SentryBuildOptions } from './types';
import { getWebpackBuildFunctionCalled } from './util';
import { createSentryBuildPluginManager } from '@sentry/bundler-plugin-core';
import { getBuildPluginOptions } from './webpackPluginOptions';
import { glob } from 'glob';

/**
* A function to do Sentry stuff for the `afterProductionBuild` Next.js hook
*/
export async function handleAfterProductionBuild(
buildInfo: { distDir: string; releaseName: string | undefined },
sentryBuildOptions: SentryBuildOptions,
): Promise<void> {
// The handleAfterProductionBuild function is only relevant if we are using Turbopack instead of Webpack, meaning we noop if we detect that we did any webpack logic
if (getWebpackBuildFunctionCalled()) {
if (sentryBuildOptions.debug) {
// eslint-disable-next-line no-console
console.debug('[@sentry/nextjs] Not running afterProductionBuild logic because Webpack context was ran.');
}
return;
}

const sentryBuildPluginManager = createSentryBuildPluginManager(
getBuildPluginOptions(sentryBuildOptions, buildInfo.releaseName, 'after-production-build', buildInfo.distDir),
{
buildTool: 'turbopack',
loggerPrefix: '[@sentry/nextjs]',
},
);

const buildArtifactsPromise = glob(
['/**/*.js', '/**/*.mjs', '/**/*.cjs', '/**/*.js.map', '/**/*.mjs.map', '/**/*.cjs.map'].map(
q => `${q}?(\\?*)?(#*)`,
), // We want to allow query and hashes strings at the end of files
{
root: buildInfo.distDir,
absolute: true,
nodir: true,
},
);

await sentryBuildPluginManager.telemetry.emitBundlerPluginExecutionSignal();
await sentryBuildPluginManager.createRelease();
await sentryBuildPluginManager.uploadSourcemaps(await buildArtifactsPromise);
await sentryBuildPluginManager.deleteArtifacts();
}
1 change: 1 addition & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ export type NextConfigObject = {
productionBrowserSourceMaps?: boolean;
// https://nextjs.org/docs/pages/api-reference/next-config-js/env
env?: Record<string, string>;
afterProductionBuild?: (metadata: { projectDir: string; distDir: string }) => Promise<void>;
};

export type SentryBuildOptions = {
Expand Down
19 changes: 19 additions & 0 deletions packages/nextjs/src/config/util.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { GLOBAL_OBJ } from '@sentry/core';
import * as fs from 'fs';
import { sync as resolveSync } from 'resolve';

Expand Down Expand Up @@ -27,3 +28,21 @@ function resolveNextjsPackageJson(): string | undefined {
return undefined;
}
}

/**
* Leaves a mark on the global scope in the Next.js build context that webpack has been executed.
*/
export function setWebpackBuildFunctionCalled(): void {
// Let the rest of the execution context know that we are using Webpack to build.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
(GLOBAL_OBJ as any)._sentryWebpackBuildFunctionCalled = true;
}

/**
* Checks whether webpack has been executed fot the current Next.js build.
*/
export function getWebpackBuildFunctionCalled(): boolean {
// Let the rest of the execution context know that we are using Webpack to build.
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/no-explicit-any
return !!(GLOBAL_OBJ as any)._sentryWebpackBuildFunctionCalled;
}
19 changes: 16 additions & 3 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import type {
WebpackConfigObjectWithModuleRules,
WebpackEntryProperty,
} from './types';
import { getWebpackPluginOptions } from './webpackPluginOptions';
import { getNextjsVersion } from './util';
import { getBuildPluginOptions } from './webpackPluginOptions';
import { getNextjsVersion, setWebpackBuildFunctionCalled } from './util';

// Next.js runs webpack 3 times, once for the client, the server, and for edge. Because we don't want to print certain
// warnings 3 times, we keep track of them here.
Expand Down Expand Up @@ -52,6 +52,8 @@ export function constructWebpackConfigFunction(
incomingConfig: WebpackConfigObject,
buildContext: BuildContext,
): WebpackConfigObject {
setWebpackBuildFunctionCalled();

const { isServer, dev: isDev, dir: projectDir } = buildContext;
const runtime = isServer ? (buildContext.nextRuntime === 'edge' ? 'edge' : 'server') : 'client';
// Default page extensions per https://github.com/vercel/next.js/blob/f1dbc9260d48c7995f6c52f8fbcc65f08e627992/packages/next/server/config-shared.ts#L161
Expand Down Expand Up @@ -393,8 +395,19 @@ export function constructWebpackConfigFunction(
}

newConfig.plugins = newConfig.plugins || [];

const mode = ({ client: 'webpack-client', server: 'webpack-nodejs', edge: 'webpack-edge' } as const)[runtime];

// We need to convert paths to posix because Glob patterns use `\` to escape
// glob characters. This clashes with Windows path separators.
// See: https://www.npmjs.com/package/glob
const projectDir = buildContext.dir.replace(/\\/g, '/');
// `.next` is the default directory
const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next';
const distDirAbsPath = path.posix.join(projectDir, distDir);

const sentryWebpackPluginInstance = sentryWebpackPlugin(
getWebpackPluginOptions(buildContext, userSentryOptions, releaseName),
getBuildPluginOptions(userSentryOptions, releaseName, mode, distDirAbsPath),
);
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
sentryWebpackPluginInstance._name = 'sentry-webpack-plugin'; // For tests and debugging. Serves no other purpose.
Expand Down
94 changes: 53 additions & 41 deletions packages/nextjs/src/config/webpackPluginOptions.ts
Original file line number Diff line number Diff line change
@@ -1,54 +1,75 @@
import * as path from 'path';
import type { SentryWebpackPluginOptions } from '@sentry/webpack-plugin';
import type { BuildContext, NextConfigObject, SentryBuildOptions } from './types';
import type { SentryBuildOptions } from './types';

/**
* Combine default and user-provided SentryWebpackPlugin options, accounting for whether we're building server files or
* client files.
*/
export function getWebpackPluginOptions(
buildContext: BuildContext,
export function getBuildPluginOptions(
sentryBuildOptions: SentryBuildOptions,
releaseName: string | undefined,
mode: 'webpack-nodejs' | 'webpack-edge' | 'webpack-client' | 'after-production-build',
distDirAbsPath: string,
): SentryWebpackPluginOptions {
const { isServer, config: userNextConfig, dir, nextRuntime } = buildContext;

const prefixInsert = !isServer ? 'Client' : nextRuntime === 'edge' ? 'Edge' : 'Node.js';

// We need to convert paths to posix because Glob patterns use `\` to escape
// glob characters. This clashes with Windows path separators.
// See: https://www.npmjs.com/package/glob
const projectDir = dir.replace(/\\/g, '/');
// `.next` is the default directory
const distDir = (userNextConfig as NextConfigObject).distDir?.replace(/\\/g, '/') ?? '.next';
const distDirAbsPath = path.posix.join(projectDir, distDir);
const loggerPrefixOverride = {
'webpack-nodejs': '[@sentry/nextjs - Node.js]',
'webpack-edge': '[@sentry/nextjs - Edge]',
'webpack-client': '[@sentry/nextjs - Client]',
'after-production-build': '[@sentry/nextjs]',
}[mode];

const sourcemapUploadAssets: string[] = [];
const sourcemapUploadIgnore: string[] = [];
const filesToDeleteAfterUpload: string[] = [];

if (isServer) {
if (mode === 'after-production-build') {
sourcemapUploadAssets.push(
path.posix.join(distDirAbsPath, 'server', '**'), // This is normally where Next.js outputs things
path.posix.join(distDirAbsPath, 'serverless', '**'), // This was the output location for serverless Next.js
path.posix.join(distDirAbsPath, '**'), // This is normally where Next.js outputs things
);
if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) {
filesToDeleteAfterUpload.push(
path.posix.join(distDirAbsPath, '**', '*.js.map'),
path.posix.join(distDirAbsPath, '**', '*.mjs.map'),
path.posix.join(distDirAbsPath, '**', '*.cjs.map'),
);
}
} else {
if (sentryBuildOptions.widenClientFileUpload) {
sourcemapUploadAssets.push(path.posix.join(distDirAbsPath, 'static', 'chunks', '**'));
} else {
if (mode === 'webpack-nodejs' || mode === 'webpack-edge') {
sourcemapUploadAssets.push(
path.posix.join(distDirAbsPath, 'static', 'chunks', 'pages', '**'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'app', '**'),
path.posix.join(distDirAbsPath, 'server', '**'), // This is normally where Next.js outputs things
path.posix.join(distDirAbsPath, 'serverless', '**'), // This was the output location for serverless Next.js
);
} else {
if (sentryBuildOptions.widenClientFileUpload) {
sourcemapUploadAssets.push(path.posix.join(distDirAbsPath, 'static', 'chunks', '**'));
} else {
sourcemapUploadAssets.push(
path.posix.join(distDirAbsPath, 'static', 'chunks', 'pages', '**'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'app', '**'),
);
}

// TODO: We should think about uploading these when `widenClientFileUpload` is `true`. They may be useful in some situations.
sourcemapUploadIgnore.push(
path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'),
);
}

// TODO: We should think about uploading these when `widenClientFileUpload` is `true`. They may be useful in some situations.
sourcemapUploadIgnore.push(
path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework-*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'framework.*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'main-*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'polyfills-*'),
path.posix.join(distDirAbsPath, 'static', 'chunks', 'webpack-*'),
);
if (sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload) {
filesToDeleteAfterUpload.push(
// We only care to delete client bundle source maps because they would be the ones being served.
// Removing the server source maps crashes Vercel builds for (thus far) unknown reasons:
// https://github.com/getsentry/sentry-javascript/issues/13099
path.posix.join(distDirAbsPath, 'static', '**', '*.js.map'),
path.posix.join(distDirAbsPath, 'static', '**', '*.mjs.map'),
path.posix.join(distDirAbsPath, 'static', '**', '*.cjs.map'),
);
}
}

return {
Expand Down Expand Up @@ -77,16 +98,7 @@ export function getWebpackPluginOptions(
},
assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets,
ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore,
filesToDeleteAfterUpload: sentryBuildOptions.sourcemaps?.deleteSourcemapsAfterUpload
? [
// We only care to delete client bundle source maps because they would be the ones being served.
// Removing the server source maps crashes Vercel builds for (thus far) unknown reasons:
// https://github.com/getsentry/sentry-javascript/issues/13099
path.posix.join(distDirAbsPath, 'static', '**', '*.js.map'),
path.posix.join(distDirAbsPath, 'static', '**', '*.mjs.map'),
path.posix.join(distDirAbsPath, 'static', '**', '*.cjs.map'),
]
: undefined,
filesToDeleteAfterUpload: filesToDeleteAfterUpload,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps,
},
release:
Expand All @@ -111,7 +123,7 @@ export function getWebpackPluginOptions(
...sentryBuildOptions.bundleSizeOptimizations,
},
_metaOptions: {
loggerPrefixOverride: `[@sentry/nextjs - ${prefixInsert}]`,
loggerPrefixOverride,
telemetry: {
metaFramework: 'nextjs',
},
Expand Down
21 changes: 21 additions & 0 deletions packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import type {
} from './types';
import { constructWebpackConfigFunction } from './webpack';
import { getNextjsVersion } from './util';
import { handleAfterProductionBuild } from './afterProductionBuild';

let showedExportModeTunnelWarning = false;

Expand Down Expand Up @@ -209,6 +210,26 @@ function getFinalConfigObject(
);
}

// Used for turbopack. Runs sourcemaps upload & release management via the `afterProductionBuild` hook.
if (incomingUserNextConfigObject.afterProductionBuild === undefined) {
incomingUserNextConfigObject.afterProductionBuild = async ({ distDir }) => {
await handleAfterProductionBuild({ releaseName, distDir }, userSentryOptions);
};
} else if (typeof incomingUserNextConfigObject.afterProductionBuild === 'function') {
incomingUserNextConfigObject.afterProductionBuild = new Proxy(incomingUserNextConfigObject.afterProductionBuild, {
async apply(target, thisArg, argArray) {
const { distDir }: { distDir: string } = argArray[0] ?? { distDir: '.next' }; // should never be undefined but to be defensive
await target.apply(thisArg, argArray);
await handleAfterProductionBuild({ releaseName, distDir }, userSentryOptions);
},
});
} else {
// eslint-disable-next-line no-console
console.warn(
'[@sentry/nextjs] The configured `afterProductionBuild` option is not a function. Will not run source map and release management logic.',
);
}

return {
...incomingUserNextConfigObject,
webpack: constructWebpackConfigFunction(incomingUserNextConfigObject, userSentryOptions, releaseName),
Expand Down
28 changes: 28 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -6464,6 +6464,11 @@
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.2.4.tgz#c0877df6e5ce227bf51754bf27da2fa5227af847"
integrity sha512-yBzRn3GEUSv1RPtE4xB4LnuH74ZxtdoRJ5cmQ9i6mzlmGDxlrnKuvem5++AolZTE9oJqAD3Tx2rd1PqmpWnLoA==

"@sentry/[email protected]":
version "3.3.0-alpha.1"
resolved "https://registry.yarnpkg.com/@sentry/babel-plugin-component-annotate/-/babel-plugin-component-annotate-3.3.0-alpha.1.tgz#4d839cfb7f313d93be88ccfce0a54b7c66fc1080"
integrity sha512-QtiIig59zHL1cVRoNaBikjF92UBpXxWq8tBe+OupP+W8U63IbLVv2C8pUPzz3QngtzzjnTY+lDFGi6LmQrmFJw==

"@sentry/[email protected]":
version "2.22.6"
resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-2.22.6.tgz#a1ea1fd43700a3ece9e7db016997e79a2782b87d"
Expand Down Expand Up @@ -6492,6 +6497,20 @@
magic-string "0.30.8"
unplugin "1.0.1"

"@sentry/[email protected]":
version "3.3.0-alpha.1"
resolved "https://registry.yarnpkg.com/@sentry/bundler-plugin-core/-/bundler-plugin-core-3.3.0-alpha.1.tgz#356d343ec9c399416cf0c193621bc1871135279c"
integrity sha512-dIbwiiuTq9RJDbltl3jrPZRVyQ/BePuEhNiP2cZ0oiQPeM9SRiZcGRZ7nmG+DBlTPp+IM51R1hhskndrVg9R1Q==
dependencies:
"@babel/core" "^7.18.5"
"@sentry/babel-plugin-component-annotate" "3.3.0-alpha.1"
"@sentry/cli" "2.42.2"
dotenv "^16.3.1"
find-up "^5.0.0"
glob "^9.3.2"
magic-string "0.30.8"
unplugin "1.0.1"

"@sentry/[email protected]":
version "2.42.2"
resolved "https://registry.yarnpkg.com/@sentry/cli-darwin/-/cli-darwin-2.42.2.tgz#a32a4f226e717122b37d9969e8d4d0e14779f720"
Expand Down Expand Up @@ -6633,6 +6652,15 @@
unplugin "1.0.1"
uuid "^9.0.0"

"@sentry/[email protected]":
version "3.3.0-alpha.1"
resolved "https://registry.yarnpkg.com/@sentry/webpack-plugin/-/webpack-plugin-3.3.0-alpha.1.tgz#6151b01ae51dfd0a7e724ea05b48c7e0090f70b0"
integrity sha512-173XxkdjL5La9KtdKxem1z2b+p6vK/ADbXy/yRIJc0q4Ayu9AVKnIQZ1GAQ4lGFxuEHgzc6eVSY+eDE6omxUxQ==
dependencies:
"@sentry/bundler-plugin-core" "3.3.0-alpha.1"
unplugin "1.0.1"
uuid "^9.0.0"

"@sigstore/protobuf-specs@^0.1.0":
version "0.1.0"
resolved "https://registry.yarnpkg.com/@sigstore/protobuf-specs/-/protobuf-specs-0.1.0.tgz#957cb64ea2f5ce527cc9cf02a096baeb0d2b99b4"
Expand Down
Loading