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 runAfterProductionCompile to upload sourcemaps and do release management #15779

Draft
wants to merge 9 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.12.0",
"@sentry/react": "9.12.0",
"@sentry/vercel-edge": "9.12.0",
"@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
138 changes: 138 additions & 0 deletions packages/nextjs/src/config/buildPluginOptions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import * as path from 'path';
import type { SentryWebpackPluginOptions } from '@sentry/webpack-plugin';
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 getBuildPluginOptions(
sentryBuildOptions: SentryBuildOptions,
releaseName: string | undefined,
mode: 'webpack-nodejs' | 'webpack-edge' | 'webpack-client' | 'after-production-build',
distDirAbsPath: string,
): SentryWebpackPluginOptions {
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[] = [];

// 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 normalizedDistDirAbsPath = distDirAbsPath.replace(/\\/g, '/');

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

return {
authToken: sentryBuildOptions.authToken,
headers: sentryBuildOptions.headers,
org: sentryBuildOptions.org,
project: sentryBuildOptions.project,
telemetry: sentryBuildOptions.telemetry,
debug: sentryBuildOptions.debug,
reactComponentAnnotation: {
...sentryBuildOptions.reactComponentAnnotation,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.reactComponentAnnotation,
},
silent: sentryBuildOptions.silent,
url: sentryBuildOptions.sentryUrl,
sourcemaps: {
disable: sentryBuildOptions.sourcemaps?.disable,
rewriteSources(source) {
if (source.startsWith('webpack://_N_E/')) {
return source.replace('webpack://_N_E/', '');
} else if (source.startsWith('webpack://')) {
return source.replace('webpack://', '');
} else {
return source;
}
},
assets: sentryBuildOptions.sourcemaps?.assets ?? sourcemapUploadAssets,
ignore: sentryBuildOptions.sourcemaps?.ignore ?? sourcemapUploadIgnore,
filesToDeleteAfterUpload: filesToDeleteAfterUpload,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.sourcemaps,
},
release:
releaseName !== undefined
? {
inject: false, // The webpack plugin's release injection breaks the `app` directory - we inject the release manually with the value injection loader instead.
name: releaseName,
create: sentryBuildOptions.release?.create,
finalize: sentryBuildOptions.release?.finalize,
dist: sentryBuildOptions.release?.dist,
vcsRemote: sentryBuildOptions.release?.vcsRemote,
setCommits: sentryBuildOptions.release?.setCommits,
deploy: sentryBuildOptions.release?.deploy,
...sentryBuildOptions.unstable_sentryWebpackPluginOptions?.release,
}
: {
inject: false,
create: false,
finalize: false,
},
bundleSizeOptimizations: {
...sentryBuildOptions.bundleSizeOptimizations,
},
_metaOptions: {
loggerPrefixOverride,
telemetry: {
metaFramework: 'nextjs',
},
},
...sentryBuildOptions.unstable_sentryWebpackPluginOptions,
};
}
61 changes: 61 additions & 0 deletions packages/nextjs/src/config/runAfterProductionCompile.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import type { SentryBuildOptions } from './types';
import { getWebpackBuildFunctionCalled } from './util';
import { getBuildPluginOptions } from './buildPluginOptions';
import { glob } from 'glob';
import { loadModule } from '@sentry/core';
import type { createSentryBuildPluginManager as createSentryBuildPluginManagerType } from '@sentry/bundler-plugin-core';

/**
* A function to do Sentry stuff for the `runAfterProductionCompile` Next.js hook
*/
export async function handleAfterProductionCompile(
buildInfo: { distDir: string; releaseName: string | undefined },
sentryBuildOptions: SentryBuildOptions,
): Promise<void> {
// The handleAfterProductionCompile 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 runAfterProductionCompile logic because Webpack context was ran.');
}
return;
}

const { createSentryBuildPluginManager } =
loadModule<{ createSentryBuildPluginManager: typeof createSentryBuildPluginManagerType }>(
'@sentry/bundler-plugin-core',
module,
) ?? {};

if (!createSentryBuildPluginManager) {
// eslint-disable-next-line no-console
console.warn(
'[@sentry/nextjs] Could not load build manager package. Will not run runAfterProductionCompile logic.',
);
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();
}
3 changes: 3 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ export type NextConfigObject = {
productionBrowserSourceMaps?: boolean;
// https://nextjs.org/docs/pages/api-reference/next-config-js/env
env?: Record<string, string>;
compiler?: {
runAfterProductionCompile?: (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 './buildPluginOptions';
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
Loading