Skip to content

Commit

Permalink
ref(nextjs): Make build-time value injection turbopack compatible (#1…
Browse files Browse the repository at this point in the history
…4081)

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.
  • Loading branch information
lforst authored Nov 6, 2024
1 parent 0c36564 commit 3750914
Show file tree
Hide file tree
Showing 14 changed files with 82 additions and 34 deletions.
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ module.exports = [
path: 'packages/browser/build/npm/esm/index.js',
import: createImport('init', 'browserTracingIntegration', 'replayIntegration', 'replayCanvasIntegration'),
gzip: true,
limit: '78.1 KB',
limit: '78.2 KB',
},
{
name: '@sentry/browser (incl. Tracing, Replay, Feedback)',
Expand Down
2 changes: 1 addition & 1 deletion packages/browser-utils/src/metrics/browserMetrics.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/* eslint-disable max-lines */
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan, startInactiveSpan } from '@sentry/core';
import { SEMANTIC_ATTRIBUTE_SENTRY_ORIGIN, getActiveSpan } from '@sentry/core';
import { setMeasurement } from '@sentry/core';
import type { Measurements, Span, SpanAttributes, StartSpanOptions } from '@sentry/types';
import { browserPerformanceTimeOrigin, getComponentName, htmlTreeAsString, logger, parseUrl } from '@sentry/utils';
Expand Down
7 changes: 5 additions & 2 deletions packages/nextjs/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export * from '@sentry/react';
export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error';

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__rewriteFramesAssetPrefixPath__: string;
_sentryRewriteFramesAssetPrefixPath: string;
};

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

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

return customDefaultIntegrations;
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/src/client/tunnelRoute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ import { GLOBAL_OBJ, dsnFromString, logger } from '@sentry/utils';
import { DEBUG_BUILD } from '../common/debug-build';

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__sentryRewritesTunnelPath__?: string;
_sentryRewritesTunnelPath?: string;
};

/**
* Applies the `tunnel` option to the Next.js SDK options based on `withSentryConfig`'s `tunnelRoute` option.
*/
export function applyTunnelRouteOption(options: BrowserOptions): void {
const tunnelRouteOption = globalWithInjectedValues.__sentryRewritesTunnelPath__;
const tunnelRouteOption = process.env._sentryRewritesTunnelPath || globalWithInjectedValues._sentryRewritesTunnelPath;
if (tunnelRouteOption && options.dsn) {
const dsnComponents = dsnFromString(options.dsn);
if (!dsnComponents) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ type OriginalStackFrameResponse = {
};

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__sentryBasePath?: string;
_sentryBasePath?: string;
};

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

let basePath = globalWithInjectedValues.__sentryBasePath ?? '';
let basePath = process.env._sentryBasePath ?? globalWithInjectedValues._sentryBasePath ?? '';

// Prefix the basepath with a slash if it doesn't have one
if (basePath !== '' && !basePath.match(/^\//)) {
Expand Down
4 changes: 3 additions & 1 deletion packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,8 @@ export type NextConfigObject = {
clientTraceMetadata?: string[];
};
productionBrowserSourceMaps?: boolean;
// https://nextjs.org/docs/pages/api-reference/next-config-js/env
env?: Record<string, string>;
};

export type SentryBuildOptions = {
Expand Down Expand Up @@ -548,7 +550,7 @@ export type ModuleRuleUseProperty = {
* Global with values we add when we inject code into people's pages, for use at runtime.
*/
export type EnhancedGlobal = typeof GLOBAL_OBJ & {
__rewriteFramesDistDir__?: string;
_sentryRewriteFramesDistDir?: string;
SENTRY_RELEASE?: { id: string };
SENTRY_RELEASES?: { [key: string]: { id: string } };
};
10 changes: 6 additions & 4 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -562,6 +562,8 @@ function setUpModuleRules(newConfig: WebpackConfigObject): WebpackConfigObjectWi
/**
* Adds loaders to inject values on the global object based on user configuration.
*/
// 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.
// 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.
function addValueInjectionLoader(
newConfig: WebpackConfigObjectWithModuleRules,
userNextConfig: NextConfigObject,
Expand All @@ -572,7 +574,7 @@ function addValueInjectionLoader(

const isomorphicValues = {
// `rewritesTunnel` set by the user in Next.js config
__sentryRewritesTunnelPath__:
_sentryRewritesTunnelPath:
userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export'
? `${userNextConfig.basePath ?? ''}${userSentryOptions.tunnelRoute}`
: undefined,
Expand All @@ -582,21 +584,21 @@ function addValueInjectionLoader(
SENTRY_RELEASE: buildContext.dev
? undefined
: { id: userSentryOptions.release?.name ?? getSentryRelease(buildContext.buildId) },
__sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
_sentryBasePath: buildContext.dev ? userNextConfig.basePath : undefined,
};

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

const clientValues = {
...isomorphicValues,
// 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.)
__rewriteFramesAssetPrefixPath__: assetPrefix
_sentryRewriteFramesAssetPrefixPath: assetPrefix
? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '')
: '',
};
Expand Down
40 changes: 40 additions & 0 deletions packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ let showedExportModeTunnelWarning = false;
* @param sentryBuildOptions Additional options to configure instrumentation and
* @returns The modified config to be exported
*/
// TODO(v9): Always return an async function here to allow us to do async things like grabbing a deterministic build ID.
export function withSentryConfig<C>(nextConfig?: C, sentryBuildOptions: SentryBuildOptions = {}): C {
const castNextConfig = (nextConfig as NextConfig) || {};
if (typeof castNextConfig === 'function') {
Expand Down Expand Up @@ -73,6 +74,8 @@ function getFinalConfigObject(
}
}

setUpBuildTimeVariables(incomingUserNextConfigObject, userSentryOptions);

const nextJsVersion = getNextjsVersion();

// 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).
Expand Down Expand Up @@ -253,6 +256,43 @@ function setUpTunnelRewriteRules(userNextConfig: NextConfigObject, tunnelPath: s
};
}

// TODO(v9): Inject the release into all the bundles. This is breaking because grabbing the build ID if the user provides
// it in `generateBuildId` (https://nextjs.org/docs/app/api-reference/next-config-js/generateBuildId) is async but we do
// not turn the next config function in the type it was passed.
function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOptions: SentryBuildOptions): void {
const assetPrefix = userNextConfig.assetPrefix || userNextConfig.basePath || '';
const basePath = userNextConfig.basePath ?? '';
const rewritesTunnelPath =
userSentryOptions.tunnelRoute !== undefined && userNextConfig.output !== 'export'
? `${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 (rewritesTunnelPath) {
buildTimeVariables._sentryRewritesTunnelPath = rewritesTunnelPath;
}

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

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

function getNextjsVersion(): string | undefined {
const nextjsPackageJsonPath = resolveNextjsPackageJson();
if (nextjsPackageJsonPath) {
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/src/edge/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export { captureUnderscoreErrorException } from '../common/pages-router-instrume
export type EdgeOptions = VercelEdgeOptions;

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__rewriteFramesDistDir__?: string;
_sentryRewriteFramesDistDir?: string;
};

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

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

if (distDirName) {
customDefaultIntegrations.push(distDirRewriteFramesIntegration({ distDirName }));
Expand Down
7 changes: 3 additions & 4 deletions packages/nextjs/src/edge/rewriteFramesIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type { IntegrationFn, StackFrame } from '@sentry/types';
import { GLOBAL_OBJ, escapeStringForRegex } from '@sentry/utils';

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__rewriteFramesDistDir__?: string;
_sentryRewriteFramesDistDir?: string;
};

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

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

if (distDirName) {
const distDirAbsPath = distDirName.replace(/(\/|\\)$/, ''); // We strip trailing slashes because "app:///_next" also doesn't have one
Expand Down
12 changes: 7 additions & 5 deletions packages/nextjs/src/server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,8 @@ export * from '@sentry/node';
export { captureUnderscoreErrorException } from '../common/pages-router-instrumentation/_error';

const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & {
__rewriteFramesDistDir__?: string;
__sentryRewritesTunnelPath__?: string;
_sentryRewriteFramesDistDir?: string;
_sentryRewritesTunnelPath?: string;
};

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

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

// Filter out transactions for requests to the tunnel route
if (
globalWithInjectedValues.__sentryRewritesTunnelPath__ &&
event.transaction === `POST ${globalWithInjectedValues.__sentryRewritesTunnelPath__}`
(globalWithInjectedValues._sentryRewritesTunnelPath &&
event.transaction === `POST ${globalWithInjectedValues._sentryRewritesTunnelPath}`) ||
(process.env._sentryRewritesTunnelPath &&
event.transaction === `POST ${process.env._sentryRewritesTunnelPath}`)
) {
return null;
}
Expand Down
4 changes: 2 additions & 2 deletions packages/nextjs/src/server/rewriteFramesIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { IntegrationFn, StackFrame } from '@sentry/types';
import { escapeStringForRegex } from '@sentry/utils';

const globalWithInjectedValues = global as typeof global & {
__rewriteFramesDistDir__?: string;
_sentryRewriteFramesDistDir?: string;
};

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

if (distDirName) {
// nextjs always puts the build directory at the project root level, which is also where you run `next start` from, so
Expand Down
2 changes: 1 addition & 1 deletion packages/nextjs/test/serverSdk.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { GLOBAL_OBJ } from '@sentry/utils';
import { init } from '../src/server';

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

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

Expand Down
14 changes: 7 additions & 7 deletions packages/nextjs/test/utils/tunnelRoute.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,16 @@ import type { BrowserOptions } from '@sentry/react';
import { applyTunnelRouteOption } from '../../src/client/tunnelRoute';

const globalWithInjectedValues = global as typeof global & {
__sentryRewritesTunnelPath__?: string;
_sentryRewritesTunnelPath?: string;
};

beforeEach(() => {
globalWithInjectedValues.__sentryRewritesTunnelPath__ = undefined;
globalWithInjectedValues._sentryRewritesTunnelPath = undefined;
});

describe('applyTunnelRouteOption()', () => {
it('Correctly applies `tunnelRoute` option when conditions are met', () => {
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
const options: any = {
dsn: 'https://11111111111111111111111111111111@o2222222.ingest.sentry.io/3333333',
} as BrowserOptions;
Expand All @@ -23,7 +23,7 @@ describe('applyTunnelRouteOption()', () => {
});

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

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

it("Doesn't `tunnelRoute` option when DSN is not a SaaS DSN", () => {
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
const options: any = {
dsn: 'https://11111111111111111111111111111111@example.com/3333333',
} as BrowserOptions;
Expand All @@ -66,7 +66,7 @@ describe('applyTunnelRouteOption()', () => {
});

it('Correctly applies `tunnelRoute` option to region DSNs', () => {
globalWithInjectedValues.__sentryRewritesTunnelPath__ = '/my-error-monitoring-route';
globalWithInjectedValues._sentryRewritesTunnelPath = '/my-error-monitoring-route';
const options: any = {
dsn: 'https://11111111111111111111111111111111@o2222222.ingest.us.sentry.io/3333333',
} as BrowserOptions;
Expand Down

0 comments on commit 3750914

Please sign in to comment.