Skip to content

feat(nextjs): Do not strip origin information from different origin stack frames #15418

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

Merged
merged 15 commits into from
Feb 24, 2025
2 changes: 1 addition & 1 deletion .size-limit.js
Original file line number Diff line number Diff line change
Expand Up @@ -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)
{
Expand Down
86 changes: 70 additions & 16 deletions packages/nextjs/src/client/clientNormalizationIntegration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<origin>/<path>/_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, '');
Comment on lines +48 to +49
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

L: That's a minor code-style comment - I would move this logic (which is shorter) upward to improve readability.

} 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;
Expand Down
21 changes: 18 additions & 3 deletions packages/nextjs/src/client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;
}
Expand Down
9 changes: 9 additions & 0 deletions packages/nextjs/src/config/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand Down
4 changes: 4 additions & 0 deletions packages/nextjs/src/config/webpack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
16 changes: 16 additions & 0 deletions packages/nextjs/src/config/withSentryConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand All @@ -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) {
Expand Down
Loading