diff --git a/.size-limit.js b/.size-limit.js index 8b506b8f683b..4903d38fef62 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -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)', diff --git a/packages/browser-utils/src/metrics/browserMetrics.ts b/packages/browser-utils/src/metrics/browserMetrics.ts index 278ce9c9784e..09714e90c11f 100644 --- a/packages/browser-utils/src/metrics/browserMetrics.ts +++ b/packages/browser-utils/src/metrics/browserMetrics.ts @@ -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'; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index c50bbce37305..a1e6aba11308 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -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 @@ -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; diff --git a/packages/nextjs/src/client/tunnelRoute.ts b/packages/nextjs/src/client/tunnelRoute.ts index 3c93b93e41f2..59ce4cfe82b7 100644 --- a/packages/nextjs/src/client/tunnelRoute.ts +++ b/packages/nextjs/src/client/tunnelRoute.ts @@ -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) { diff --git a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts index 6c37859a851d..143bf6fef6ef 100644 --- a/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts +++ b/packages/nextjs/src/common/devErrorSymbolicationEventProcessor.ts @@ -11,7 +11,7 @@ type OriginalStackFrameResponse = { }; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { - __sentryBasePath?: string; + _sentryBasePath?: string; }; async function resolveStackFrame( @@ -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(/^\//)) { diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index d887369606b6..14be35215cf2 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -47,6 +47,8 @@ export type NextConfigObject = { clientTraceMetadata?: string[]; }; productionBrowserSourceMaps?: boolean; + // https://nextjs.org/docs/pages/api-reference/next-config-js/env + env?: Record; }; export type SentryBuildOptions = { @@ -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 } }; }; diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index 6b96b96ecec1..b83008a4817e 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -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, @@ -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, @@ -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(/\/$/, '') : '', }; diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 4f5205fecfcb..539e75c20596 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -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(nextConfig?: C, sentryBuildOptions: SentryBuildOptions = {}): C { const castNextConfig = (nextConfig as NextConfig) || {}; if (typeof castNextConfig === 'function') { @@ -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). @@ -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 = { + // 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) { diff --git a/packages/nextjs/src/edge/index.ts b/packages/nextjs/src/edge/index.ts index fff4236bf3be..5bfc8cca054b 100644 --- a/packages/nextjs/src/edge/index.ts +++ b/packages/nextjs/src/edge/index.ts @@ -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. */ @@ -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 })); diff --git a/packages/nextjs/src/edge/rewriteFramesIntegration.ts b/packages/nextjs/src/edge/rewriteFramesIntegration.ts index 84d5a41b0922..15a541311ed1 100644 --- a/packages/nextjs/src/edge/rewriteFramesIntegration.ts +++ b/packages/nextjs/src/edge/rewriteFramesIntegration.ts @@ -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; @@ -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 diff --git a/packages/nextjs/src/server/index.ts b/packages/nextjs/src/server/index.ts index 7aade8cbd5c3..bdff0de922b2 100644 --- a/packages/nextjs/src/server/index.ts +++ b/packages/nextjs/src/server/index.ts @@ -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; }; /** @@ -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 })); } @@ -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; } diff --git a/packages/nextjs/src/server/rewriteFramesIntegration.ts b/packages/nextjs/src/server/rewriteFramesIntegration.ts index 6438ccb0d922..33bc7d90cb99 100644 --- a/packages/nextjs/src/server/rewriteFramesIntegration.ts +++ b/packages/nextjs/src/server/rewriteFramesIntegration.ts @@ -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; @@ -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 diff --git a/packages/nextjs/test/serverSdk.test.ts b/packages/nextjs/test/serverSdk.test.ts index 1129bcdbbf2b..fed88fe25d33 100644 --- a/packages/nextjs/test/serverSdk.test.ts +++ b/packages/nextjs/test/serverSdk.test.ts @@ -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'); diff --git a/packages/nextjs/test/utils/tunnelRoute.test.ts b/packages/nextjs/test/utils/tunnelRoute.test.ts index 576898c061b2..05aa992f39e6 100644 --- a/packages/nextjs/test/utils/tunnelRoute.test.ts +++ b/packages/nextjs/test/utils/tunnelRoute.test.ts @@ -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; @@ -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; @@ -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; @@ -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; @@ -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;