diff --git a/.size-limit.js b/.size-limit.js index 157c1243021e..08adf5a80c29 100644 --- a/.size-limit.js +++ b/.size-limit.js @@ -210,7 +210,7 @@ module.exports = [ import: createImport('init'), ignore: ['next/router', 'next/constants'], gzip: true, - limit: '40 KB', + limit: '41 KB', }, // SvelteKit SDK (ESM) { diff --git a/packages/nextjs/src/client/clientNormalizationIntegration.ts b/packages/nextjs/src/client/clientNormalizationIntegration.ts index e4bbb4881bc3..a7cd2c356f4e 100644 --- a/packages/nextjs/src/client/clientNormalizationIntegration.ts +++ b/packages/nextjs/src/client/clientNormalizationIntegration.ts @@ -2,30 +2,84 @@ import { rewriteFramesIntegration } from '@sentry/browser'; import { defineIntegration } from '@sentry/core'; export const nextjsClientStackFrameNormalizationIntegration = defineIntegration( - ({ assetPrefixPath }: { assetPrefixPath: string }) => { + ({ + assetPrefix, + basePath, + rewriteFramesAssetPrefixPath, + experimentalThirdPartyOriginStackFrames, + }: { + assetPrefix?: string; + basePath?: string; + rewriteFramesAssetPrefixPath: string; + experimentalThirdPartyOriginStackFrames: boolean; + }) => { const rewriteFramesInstance = rewriteFramesIntegration({ // Turn `//_next/static/...` into `app:///_next/static/...` iteratee: frame => { - try { - const { origin } = new URL(frame.filename as string); - frame.filename = frame.filename?.replace(origin, 'app://').replace(assetPrefixPath, ''); - } catch (err) { - // Filename wasn't a properly formed URL, so there's nothing we can do + if (experimentalThirdPartyOriginStackFrames) { + // Not sure why but access to global WINDOW from @sentry/Browser causes hideous ci errors + // eslint-disable-next-line no-restricted-globals + const windowOrigin = typeof window !== 'undefined' && window.location ? window.location.origin : ''; + // A filename starting with the local origin and not ending with JS is most likely JS in HTML which we do not want to rewrite + if (frame.filename?.startsWith(windowOrigin) && !frame.filename.endsWith('.js')) { + return frame; + } + + if (assetPrefix) { + // If the user defined an asset prefix, we need to strip it so that we can match it with uploaded sourcemaps. + // assetPrefix always takes priority over basePath. + if (frame.filename?.startsWith(assetPrefix)) { + frame.filename = frame.filename.replace(assetPrefix, 'app://'); + } + } else if (basePath) { + // If the user defined a base path, we need to strip it to match with uploaded sourcemaps. + // We should only do this for same-origin filenames though, so that third party assets are not rewritten. + try { + const { origin: frameOrigin } = new URL(frame.filename as string); + if (frameOrigin === windowOrigin) { + frame.filename = frame.filename?.replace(frameOrigin, 'app://').replace(basePath, ''); + } + } catch (err) { + // Filename wasn't a properly formed URL, so there's nothing we can do + } + } + } else { + try { + const { origin } = new URL(frame.filename as string); + frame.filename = frame.filename?.replace(origin, 'app://').replace(rewriteFramesAssetPrefixPath, ''); + } catch (err) { + // Filename wasn't a properly formed URL, so there's nothing we can do + } } // We need to URI-decode the filename because Next.js has wildcard routes like "/users/[id].js" which show up as "/users/%5id%5.js" in Error stacktraces. // The corresponding sources that Next.js generates have proper brackets so we also need proper brackets in the frame so that source map resolving works. - if (frame.filename?.startsWith('app:///_next')) { - frame.filename = decodeURI(frame.filename); - } + if (experimentalThirdPartyOriginStackFrames) { + if (frame.filename?.includes('/_next')) { + frame.filename = decodeURI(frame.filename); + } + + if ( + frame.filename?.match( + /\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ) + ) { + // We don't care about these frames. It's Next.js internal code. + frame.in_app = false; + } + } else { + if (frame.filename?.startsWith('app:///_next')) { + frame.filename = decodeURI(frame.filename); + } - if ( - frame.filename?.match( - /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, - ) - ) { - // We don't care about these frames. It's Next.js internal code. - frame.in_app = false; + if ( + frame.filename?.match( + /^app:\/\/\/_next\/static\/chunks\/(main-|main-app-|polyfills-|webpack-|framework-|framework\.)[0-9a-f]+\.js$/, + ) + ) { + // We don't care about these frames. It's Next.js internal code. + frame.in_app = false; + } } return frame; diff --git a/packages/nextjs/src/client/index.ts b/packages/nextjs/src/client/index.ts index 163e29f0b9a7..c8e6d21837fd 100644 --- a/packages/nextjs/src/client/index.ts +++ b/packages/nextjs/src/client/index.ts @@ -17,6 +17,9 @@ export { browserTracingIntegration } from './browserTracingIntegration'; const globalWithInjectedValues = GLOBAL_OBJ as typeof GLOBAL_OBJ & { _sentryRewriteFramesAssetPrefixPath: string; + _sentryAssetPrefix?: string; + _sentryBasePath?: string; + _experimentalThirdPartyOriginStackFrames?: string; }; // Treeshakable guard to remove all code related to tracing @@ -67,13 +70,25 @@ function getDefaultIntegrations(options: BrowserOptions): Integration[] { customDefaultIntegrations.push(browserTracingIntegration()); } - // This value is injected at build time, based on the output directory specified in the build config. Though a default + // These values are 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 = + const rewriteFramesAssetPrefixPath = process.env._sentryRewriteFramesAssetPrefixPath || globalWithInjectedValues._sentryRewriteFramesAssetPrefixPath || ''; - customDefaultIntegrations.push(nextjsClientStackFrameNormalizationIntegration({ assetPrefixPath })); + const assetPrefix = process.env._sentryAssetPrefix || globalWithInjectedValues._sentryAssetPrefix; + const basePath = process.env._sentryBasePath || globalWithInjectedValues._sentryBasePath; + const experimentalThirdPartyOriginStackFrames = + process.env._experimentalThirdPartyOriginStackFrames === 'true' || + globalWithInjectedValues._experimentalThirdPartyOriginStackFrames === 'true'; + customDefaultIntegrations.push( + nextjsClientStackFrameNormalizationIntegration({ + assetPrefix, + basePath, + rewriteFramesAssetPrefixPath, + experimentalThirdPartyOriginStackFrames, + }), + ); return customDefaultIntegrations; } diff --git a/packages/nextjs/src/config/types.ts b/packages/nextjs/src/config/types.ts index a747684cc753..965233d08b76 100644 --- a/packages/nextjs/src/config/types.ts +++ b/packages/nextjs/src/config/types.ts @@ -439,6 +439,15 @@ export type SentryBuildOptions = { * Defaults to `false`. */ automaticVercelMonitors?: boolean; + + /** + * Contains a set of experimental flags that might change in future releases. These flags enable + * features that are still in development and may be modified, renamed, or removed without notice. + * Use with caution in production environments. + */ + _experimental?: Partial<{ + thirdPartyOriginStackFrames: boolean; + }>; }; export type NextConfigFunction = ( diff --git a/packages/nextjs/src/config/webpack.ts b/packages/nextjs/src/config/webpack.ts index f82bb4a0476e..5ff02da355a1 100644 --- a/packages/nextjs/src/config/webpack.ts +++ b/packages/nextjs/src/config/webpack.ts @@ -624,6 +624,10 @@ function addValueInjectionLoader( _sentryRewriteFramesAssetPrefixPath: assetPrefix ? new URL(assetPrefix, 'http://dogs.are.great').pathname.replace(/\/$/, '') : '', + _sentryAssetPrefix: userNextConfig.assetPrefix, + _sentryExperimentalThirdPartyOriginStackFrames: userSentryOptions._experimental?.thirdPartyOriginStackFrames + ? 'true' + : undefined, }; if (buildContext.isServer) { diff --git a/packages/nextjs/src/config/withSentryConfig.ts b/packages/nextjs/src/config/withSentryConfig.ts index 8f57f51a8c58..ab15d1c86c0d 100644 --- a/packages/nextjs/src/config/withSentryConfig.ts +++ b/packages/nextjs/src/config/withSentryConfig.ts @@ -268,6 +268,14 @@ function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOpt : '', }; + if (userNextConfig.assetPrefix) { + buildTimeVariables._assetsPrefix = userNextConfig.assetPrefix; + } + + if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) { + buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true'; + } + if (rewritesTunnelPath) { buildTimeVariables._sentryRewritesTunnelPath = rewritesTunnelPath; } @@ -276,6 +284,14 @@ function setUpBuildTimeVariables(userNextConfig: NextConfigObject, userSentryOpt buildTimeVariables._sentryBasePath = basePath; } + if (userNextConfig.assetPrefix) { + buildTimeVariables._sentryAssetPrefix = userNextConfig.assetPrefix; + } + + if (userSentryOptions._experimental?.thirdPartyOriginStackFrames) { + buildTimeVariables._experimentalThirdPartyOriginStackFrames = 'true'; + } + if (typeof userNextConfig.env === 'object') { userNextConfig.env = { ...buildTimeVariables, ...userNextConfig.env }; } else if (userNextConfig.env === undefined) {