Skip to content

Commit 3750914

Browse files
authored
ref(nextjs): Make build-time value injection turbopack compatible (#14081)
Ref: #8105 To inject build-time variables, in addition to doing so via a custom loader, we will be injecting them via the `env` option. Caveat: We are currently using the Next.js build ID as a release name. This build id is passed to the `webpack` option. Since the `webpack` option doesn't exist for turbopack we don't have access to the build ID. For now we will simply not inject a release name, which may be better anyhow since turbopack is currently only stable for dev.
1 parent 0c36564 commit 3750914

14 files changed

+82
-34
lines changed

.size-limit.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ module.exports = [
7979
path: 'packages/browser/build/npm/esm/index.js',
8080
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
8181
gzip: true,
82-
limit: '78.1 KB',
82+
limit: '78.2 KB',
8383
},
8484
{
8585
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',

packages/browser-utils/src/metrics/browserMetrics.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/* eslint-disable max-lines */
2-
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan, startInactiveSpan } from '@sentry/core';
2+
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan } from '@sentry/core';
33
import { setMeasurement } from '@sentry/core';
44
import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/types';
55
import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/utils';

packages/nextjs/src/client/index.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export * from '@sentry/react';
1616
export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error';
1717

1818
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
19-
__rewriteFramesAssetPrefixPath__: string;
19+
_sentryRewriteFramesAssetPrefixPath: string;
2020
};
2121

2222
// Treeshakable guard to remove all code related to tracing
@@ -64,7 +64,10 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] {
6464

6565
// This value is injected at build time, based on the output directory specified in the build config. Though a default
6666
// is set there, we set it here as well, just in case something has gone wrong with the injection.
67-
const assetPrefixPath = globalWithInjectedValues.__rewriteFramesAssetPrefixPath__ || '';
67+
const assetPrefixPath =
68+
process.env._sentryRewriteFramesAssetPrefixPath ||
69+
globalWithInjectedValues._sentryRewriteFramesAssetPrefixPath ||
70+
'';
6871
customDefaultIntegrations.push(nextjsClientStackFrameNormalizationIntegration({ assetPrefixPath }));
6972

7073
return customDefaultIntegrations;

packages/nextjs/src/client/tunnelRoute.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@ import { GLOBAL_OBJ, dsnFromString, logger } from '@sentry/utils';
44
import { DEBUG_BUILD } from '../common/debug-build';
55

66
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
7-
__sentryRewritesTunnelPath__?: string;
7+
_sentryRewritesTunnelPath?: string;
88
};
99

1010
/**
1111
* Applies the `tunnel` option to the Next.js SDK options based on `withSentryConfig`'s `tunnelRoute` option.
1212
*/
1313
export function applyTunnelRouteOption(options: BrowserOptions): void {
14-
const tunnelRouteOption = globalWithInjectedValues.__sentryRewritesTunnelPath__;
14+
const tunnelRouteOption = process.env._sentryRewritesTunnelPath || globalWithInjectedValues._sentryRewritesTunnelPath;
1515
if (tunnelRouteOption && options.dsn) {
1616
const dsnComponents = dsnFromString(options.dsn);
1717
if (!dsnComponents) {

packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ type OriginalStackFrameResponse = {
1111
};
1212

1313
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
14-
__sentryBasePath?: string;
14+
_sentryBasePath?: string;
1515
};
1616

1717
async function resolveStackFrame(
@@ -32,7 +32,7 @@ async function resolveStackFrame(
3232
params.append(key, (frame[key as keyof typeof frame] ?? '').toString());
3333
});
3434

35-
let basePath = globalWithInjectedValues.__sentryBasePath ?? '';
35+
let basePath = process.env._sentryBasePath ?? globalWithInjectedValues._sentryBasePath ?? '';
3636

3737
// Prefix the basepath with a slash if it doesn't have one
3838
if (basePath !== '' && !basePath.match(/^\//)) {

packages/nextjs/src/config/types.ts

+3-1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,8 @@ export type NextConfigObject = {
4747
clientTraceMetadata?: string[];
4848
};
4949
productionBrowserSourceMaps?: boolean;
50+
// https://nextjs.org/docs/pages/api-reference/next-config-js/env
51+
env?: Record<string, string>;
5052
};
5153

5254
export type SentryBuildOptions = {
@@ -548,7 +550,7 @@ export type ModuleRuleUseProperty = {
548550
* Global with values we add when we inject code into people's pages, for use at runtime.
549551
*/
550552
export type EnhancedGlobal = typeof GLOBAL_OBJ & {
551-
__rewriteFramesDistDir__?: string;
553+
_sentryRewriteFramesDistDir?: string;
552554
SENTRY_RELEASE?: { id: string };
553555
SENTRY_RELEASES?: { [key: string]: { id: string } };
554556
};

packages/nextjs/src/config/webpack.ts

+6-4
Original file line numberDiff line numberDiff line change
@@ -562,6 +562,8 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
562562
/**
563563
* Adds loaders to inject values on the global object based on user configuration.
564564
*/
565+
// TODO(v9): Remove this loader and replace it with a nextConfig.env (https://web.archive.org/web/20240917153554/https://nextjs.org/docs/app/api-reference/next-config-js/env) or define based (https://github.com/vercel/next.js/discussions/71476) approach.
566+
// In order to remove this loader though we need to make sure the minimum supported Next.js version includes this PR (https://github.com/vercel/next.js/pull/61194), otherwise the nextConfig.env based approach will not work, as our SDK code is not processed by Next.js.
565567
function addValueInjectionLoader(
566568
newConfig: WebpackConfigObjectWithModuleRules,
567569
userNextConfig: NextConfigObject,
@@ -572,7 +574,7 @@ function addValueInjectionLoader(
572574

573575
const isomorphicValues = {
574576
// `rewritesTunnel` set by the user in Next.js config
575-
__sentryRewritesTunnelPath__:
577+
_sentryRewritesTunnelPath:
576578
userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export'
577579
? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}`
578580
: undefined,
@@ -582,21 +584,21 @@ function addValueInjectionLoader(
582584
SENTRY_RELEASE: buildContext.dev
583585
? undefined
584586
: { id: userSentryOptions.release?.name ?? getSentryRelease(buildContext.buildId) },
585-
__sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
587+
_sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
586588
};
587589

588590
const serverValues = {
589591
...isomorphicValues,
590592
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
591593
// characters)
592-
__rewriteFramesDistDir__: userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
594+
_sentryRewriteFramesDistDir: userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
593595
};
594596

595597
const clientValues = {
596598
...isomorphicValues,
597599
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
598600
// `assetPrefix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
599-
__rewriteFramesAssetPrefixPath__: assetPrefix
601+
_sentryRewriteFramesAssetPrefixPath: assetPrefix
600602
? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
601603
: '',
602604
};

packages/nextjs/src/config/withSentryConfig.ts

+40
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ let showedExportModeTunnelWarning = false;
2020
* @param sentryBuildOptions Additional options to configure instrumentation and
2121
* @returns The modified config to be exported
2222
*/
23+
// TODO(v9): Always return an async function here to allow us to do async things like grabbing a deterministic build ID.
2324
export function withSentryConfig<C>(nextConfig?: C, sentryBuildOptions: SentryBuildOptions = {}): C {
2425
const castNextConfig = (nextConfig as NextConfig) || {};
2526
if (typeof castNextConfig === 'function') {
@@ -73,6 +74,8 @@ function getFinalConfigObject(
7374
}
7475
}
7576

77+
setUpBuildTimeVariables(incomingUserNextConfigObject, userSentryOptions);
78+
7679
const nextJsVersion = getNextjsVersion();
7780

7881
// Add the `clientTraceMetadata` experimental option based on Next.js version. The option got introduced in Next.js version 15.0.0 (actually 14.3.0-canary.64).
@@ -253,6 +256,43 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s
253256
};
254257
}
255258

259+
// TODO(v9): Inject the release into all the bundles. This is breaking because grabbing the build ID if the user provides
260+
// it in `generateBuildId` (https://nextjs.org/docs/app/api-reference/next-config-js/generateBuildId) is async but we do
261+
// not turn the next config function in the type it was passed.
262+
function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOptions: SentryBuildOptions): void {
263+
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
264+
const basePath = userNextConfig.basePath ?? '';
265+
const rewritesTunnelPath =
266+
userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export'
267+
? `${basePath}${userSentryOptions.tunnelRoute}`
268+
: undefined;
269+
270+
const buildTimeVariables: Record<string, string> = {
271+
// Make sure that if we have a windows path, the backslashes are interpreted as such (rather than as escape
272+
// characters)
273+
_sentryRewriteFramesDistDir: userNextConfig.distDir?.replace(/\\/g, '\\\\') || '.next',
274+
// Get the path part of `assetPrefix`, minus any trailing slash. (We use a placeholder for the origin if
275+
// `assetPrefix` doesn't include one. Since we only care about the path, it doesn't matter what it is.)
276+
_sentryRewriteFramesAssetPrefixPath: assetPrefix
277+
? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
278+
: '',
279+
};
280+
281+
if (rewritesTunnelPath) {
282+
buildTimeVariables._sentryRewritesTunnelPath = rewritesTunnelPath;
283+
}
284+
285+
if (basePath) {
286+
buildTimeVariables._sentryBasePath = basePath;
287+
}
288+
289+
if (typeof userNextConfig.env === 'object') {
290+
userNextConfig.env = { ...buildTimeVariables, ...userNextConfig.env };
291+
} else if (userNextConfig.env === undefined) {
292+
userNextConfig.env = buildTimeVariables;
293+
}
294+
}
295+
256296
function getNextjsVersion(): string | undefined {
257297
const nextjsPackageJsonPath = resolveNextjsPackageJson();
258298
if (nextjsPackageJsonPath) {

packages/nextjs/src/edge/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ export { captureUnderscoreErrorException } from '../common/pages-router-instrume
2121
export type EdgeOptions = VercelEdgeOptions;
2222

2323
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
24-
__rewriteFramesDistDir__?: string;
24+
_sentryRewriteFramesDistDir?: string;
2525
};
2626

2727
/** Inits the Sentry NextJS SDK on the Edge Runtime. */
@@ -36,7 +36,7 @@ export function init(options: VercelEdgeOptions = {}): void {
3636

3737
// This value is injected at build time, based on the output directory specified in the build config. Though a default
3838
// is set there, we set it here as well, just in case something has gone wrong with the injection.
39-
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
39+
const distDirName = process.env._sentryRewriteFramesDistDir || globalWithInjectedValues._sentryRewriteFramesDistDir;
4040

4141
if (distDirName) {
4242
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));

packages/nextjs/src/edge/rewriteFramesIntegration.ts

+3-4
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { IntegrationFn, StackFrame } from '@sentry/types';
33
import { GLOBAL_OBJ, escapeStringForRegex } from '@sentry/utils';
44

55
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
6-
__rewriteFramesDistDir__?: string;
6+
_sentryRewriteFramesDistDir?: string;
77
};
88

99
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
@@ -14,9 +14,8 @@ interface RewriteFramesOptions {
1414
}
1515

1616
export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
17-
// This value is injected at build time, based on the output directory specified in the build config. Though a default
18-
// is set there, we set it here as well, just in case something has gone wrong with the injection.
19-
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
17+
// This value is injected at build time, based on the output directory specified in the build config.
18+
const distDirName = process.env._sentryRewriteFramesDistDir || globalWithInjectedValues._sentryRewriteFramesDistDir;
2019

2120
if (distDirName) {
2221
const distDirAbsPath = distDirName.replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one

packages/nextjs/src/server/index.ts

+7-5
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ export * from '@sentry/node';
4242
export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error';
4343

4444
const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
45-
__rewriteFramesDistDir__?: string;
46-
__sentryRewritesTunnelPath__?: string;
45+
_sentryRewriteFramesDistDir?: string;
46+
_sentryRewritesTunnelPath?: string;
4747
};
4848

4949
/**
@@ -109,7 +109,7 @@ export function init(options: NodeOptions): NodeClient | undefined {
109109

110110
// This value is injected at build time, based on the output directory specified in the build config. Though a default
111111
// is set there, we set it here as well, just in case something has gone wrong with the injection.
112-
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
112+
const distDirName = process.env._sentryRewriteFramesDistDir || globalWithInjectedValues._sentryRewriteFramesDistDir;
113113
if (distDirName) {
114114
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
115115
}
@@ -212,8 +212,10 @@ export function init(options: NodeOptions): NodeClient | undefined {
212212

213213
// Filter out transactions for requests to the tunnel route
214214
if (
215-
globalWithInjectedValues.__sentryRewritesTunnelPath__ &&
216-
event.transaction === `POST ${globalWithInjectedValues.__sentryRewritesTunnelPath__}`
215+
(globalWithInjectedValues._sentryRewritesTunnelPath &&
216+
event.transaction === `POST ${globalWithInjectedValues._sentryRewritesTunnelPath}`) ||
217+
(process.env._sentryRewritesTunnelPath &&
218+
event.transaction === `POST ${process.env._sentryRewritesTunnelPath}`)
217219
) {
218220
return null;
219221
}

packages/nextjs/src/server/rewriteFramesIntegration.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { IntegrationFn, StackFrame } from '@sentry/types';
44
import { escapeStringForRegex } from '@sentry/utils';
55

66
const globalWithInjectedValues = global as typeof global & {
7-
__rewriteFramesDistDir__?: string;
7+
_sentryRewriteFramesDistDir?: string;
88
};
99

1010
type StackFrameIteratee = (frame: StackFrame) => StackFrame;
@@ -17,7 +17,7 @@ interface RewriteFramesOptions {
1717
export const customRewriteFramesIntegration = ((options?: RewriteFramesOptions) => {
1818
// This value is injected at build time, based on the output directory specified in the build config. Though a default
1919
// is set there, we set it here as well, just in case something has gone wrong with the injection.
20-
const distDirName = globalWithInjectedValues.__rewriteFramesDistDir__;
20+
const distDirName = process.env._sentryRewriteFramesDistDir || globalWithInjectedValues._sentryRewriteFramesDistDir;
2121

2222
if (distDirName) {
2323
// nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so

packages/nextjs/test/serverSdk.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { GLOBAL_OBJ } from '@sentry/utils';
66
import { init } from '../src/server';
77

88
// normally this is set as part of the build process, so mock it here
9-
(GLOBAL_OBJ as typeof GLOBAL_OBJ & { __rewriteFramesDistDir__: string }).__rewriteFramesDistDir__ = '.next';
9+
(GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesDistDir: string })._sentryRewriteFramesDistDir = '.next';
1010

1111
const nodeInit = jest.spyOn(SentryNode, 'init');
1212

packages/nextjs/test/utils/tunnelRoute.test.ts

+7-7
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,16 @@ import type { BrowserOptions } from '@sentry/react';
33
import { applyTunnelRouteOption } from '../../src/client/tunnelRoute';
44

55
const globalWithInjectedValues = global as typeof global & {
6-
__sentryRewritesTunnelPath__?: string;
6+
_sentryRewritesTunnelPath?: string;
77
};
88

99
beforeEach(() => {
10-
globalWithInjectedValues.__sentryRewritesTunnelPath__ = undefined;
10+
globalWithInjectedValues._sentryRewritesTunnelPath = undefined;
1111
});
1212

1313
describe('applyTunnelRouteOption()', () => {
1414
it('Correctly applies `tunnelRoute` option when conditions are met', () => {
15-
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
15+
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
1616
const options: any = {
1717
dsn: 'https://[email protected]/3333333',
1818
} as BrowserOptions;
@@ -23,7 +23,7 @@ describe('applyTunnelRouteOption()', () => {
2323
});
2424

2525
it("Doesn't apply `tunnelRoute` when DSN is missing", () => {
26-
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
26+
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
2727
const options: any = {
2828
// no dsn
2929
} as BrowserOptions;
@@ -34,7 +34,7 @@ describe('applyTunnelRouteOption()', () => {
3434
});
3535

3636
it("Doesn't apply `tunnelRoute` when DSN is invalid", () => {
37-
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
37+
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
3838
const options: any = {
3939
dsn: 'invalidDsn',
4040
} as BrowserOptions;
@@ -55,7 +55,7 @@ describe('applyTunnelRouteOption()', () => {
5555
});
5656

5757
it("Doesn't `tunnelRoute` option when DSN is not a SaaS DSN", () => {
58-
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
58+
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
5959
const options: any = {
6060
dsn: 'https://[email protected]/3333333',
6161
} as BrowserOptions;
@@ -66,7 +66,7 @@ describe('applyTunnelRouteOption()', () => {
6666
});
6767

6868
it('Correctly applies `tunnelRoute` option to region DSNs', () => {
69-
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
69+
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
7070
const options: any = {
7171
dsn: 'https://[email protected]/3333333',
7272
} as BrowserOptions;

0 commit comments

Comments
 (0)