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

ref(nextjs): Make build-time value injection turbopack compatible #14081

Merged
merged 9 commits into from
Nov 6, 2024
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
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
Loading